auto-save 2026-05-13 20:29 (~9)
This commit is contained in:
157
api/main.py
157
api/main.py
@@ -1584,6 +1584,66 @@ def video_seconds(duration: float) -> str:
|
||||
return "12"
|
||||
|
||||
|
||||
def resolve_video_model(raw: str | None) -> str:
|
||||
requested = (raw or VIDEO_MODEL or "seedance").strip()
|
||||
lowered = requested.lower()
|
||||
if lowered in {"sora", "sora-2", "sora_2"}:
|
||||
raise HTTPException(400, "Sora 已停用,请选择 Seedance / Kling / Veo 3")
|
||||
return VIDEO_MODEL_ALIASES.get(lowered, requested)
|
||||
|
||||
|
||||
def normalize_video_status(status: str | None) -> Literal["queued", "in_progress", "completed", "failed"]:
|
||||
s = (status or "queued").lower()
|
||||
if s in {"completed", "complete", "succeeded", "success", "done"}:
|
||||
return "completed"
|
||||
if s in {"failed", "failure", "error", "cancelled", "canceled", "expired"}:
|
||||
return "failed"
|
||||
if s in {"running", "processing", "in_progress", "generating", "started"}:
|
||||
return "in_progress"
|
||||
return "queued"
|
||||
|
||||
|
||||
def video_progress(data: dict, fallback: int) -> int:
|
||||
raw = data.get("progress", data.get("percentage", data.get("percent", fallback)))
|
||||
try:
|
||||
value = int(float(raw))
|
||||
except Exception:
|
||||
value = fallback
|
||||
return max(0, min(100, value))
|
||||
|
||||
|
||||
def video_url_from_response(data: dict) -> str:
|
||||
for key in ("url", "video_url", "output_url", "download_url"):
|
||||
v = data.get(key)
|
||||
if isinstance(v, str) and v:
|
||||
return v
|
||||
arr = data.get("data")
|
||||
if isinstance(arr, list) and arr:
|
||||
first = arr[0]
|
||||
if isinstance(first, dict):
|
||||
for key in ("url", "video_url", "output_url", "download_url"):
|
||||
v = first.get(key)
|
||||
if isinstance(v, str) and v:
|
||||
return v
|
||||
output = data.get("output")
|
||||
if isinstance(output, dict):
|
||||
for key in ("url", "video_url", "download_url"):
|
||||
v = output.get(key)
|
||||
if isinstance(v, str) and v:
|
||||
return v
|
||||
return ""
|
||||
|
||||
|
||||
def download_generated_video(client, base: str, headers: dict, provider_id: str, direct_url: str, out_mp4: Path) -> None:
|
||||
if direct_url:
|
||||
url = direct_url if direct_url.startswith("http") else f"{base}{direct_url if direct_url.startswith('/') else '/' + direct_url}"
|
||||
r = client.get(url, headers=headers if url.startswith(base) else None)
|
||||
else:
|
||||
r = client.get(f"{base}/videos/{provider_id}/content", headers=headers)
|
||||
r.raise_for_status()
|
||||
out_mp4.write_bytes(r.content)
|
||||
|
||||
|
||||
def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_path: Path, prompt: str, model: str, seconds: str, size: str) -> None:
|
||||
import httpx
|
||||
|
||||
@@ -1594,55 +1654,52 @@ def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_pa
|
||||
headers = {"Authorization": f"Bearer {LLM_API_KEY}"}
|
||||
|
||||
try:
|
||||
prepare_video_reference(ref_path, ref_img)
|
||||
update_generated_video(job_id, local_id, status="in_progress", progress=5)
|
||||
with httpx.Client(timeout=120) as client:
|
||||
with ref_img.open("rb") as fh:
|
||||
create = client.post(
|
||||
f"{base}/videos",
|
||||
headers=headers,
|
||||
data={
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"seconds": seconds,
|
||||
"size": size,
|
||||
},
|
||||
files={"input_reference": ("reference.jpg", fh, "image/jpeg")},
|
||||
)
|
||||
create.raise_for_status()
|
||||
data = create.json()
|
||||
video_api_id = data.get("id") or provider_id
|
||||
update_generated_video(job_id, local_id, provider_id=video_api_id, status=data.get("status", "queued"), progress=int(data.get("progress") or 5))
|
||||
prepare_video_reference(ref_path, ref_img)
|
||||
update_generated_video(job_id, local_id, status="in_progress", progress=5)
|
||||
with httpx.Client(timeout=120) as client:
|
||||
payload = {"model": model, "prompt": prompt, "size": size}
|
||||
payload[VIDEO_DURATION_FIELD] = seconds
|
||||
with ref_img.open("rb") as fh:
|
||||
create = client.post(
|
||||
f"{base}/videos",
|
||||
headers=headers,
|
||||
data=payload,
|
||||
files={"input_reference": ("reference.jpg", fh, "image/jpeg")},
|
||||
)
|
||||
create.raise_for_status()
|
||||
data = create.json()
|
||||
video_api_id = data.get("id") or provider_id or local_id
|
||||
status = normalize_video_status(data.get("status"))
|
||||
progress = video_progress(data, 5)
|
||||
direct_url = video_url_from_response(data)
|
||||
update_generated_video(job_id, local_id, provider_id=video_api_id, status=status, progress=progress)
|
||||
|
||||
status = data.get("status", "queued")
|
||||
progress = int(data.get("progress") or 5)
|
||||
deadline = time.time() + 420
|
||||
while status in {"queued", "in_progress"} and time.time() < deadline:
|
||||
time.sleep(8)
|
||||
poll = client.get(f"{base}/videos/{video_api_id}", headers=headers)
|
||||
poll.raise_for_status()
|
||||
pdata = poll.json()
|
||||
status = pdata.get("status", status)
|
||||
progress = int(pdata.get("progress") or progress)
|
||||
update_generated_video(job_id, local_id, status=status, progress=progress)
|
||||
deadline = time.time() + 420
|
||||
while status in {"queued", "in_progress"} and time.time() < deadline:
|
||||
time.sleep(8)
|
||||
poll = client.get(f"{base}/videos/{video_api_id}", headers=headers)
|
||||
poll.raise_for_status()
|
||||
pdata = poll.json()
|
||||
status = normalize_video_status(pdata.get("status"))
|
||||
progress = video_progress(pdata, progress)
|
||||
direct_url = video_url_from_response(pdata) or direct_url
|
||||
update_generated_video(job_id, local_id, status=status, progress=progress)
|
||||
|
||||
if status != "completed":
|
||||
update_generated_video(job_id, local_id, status="failed", error=f"video status: {status}", progress=progress)
|
||||
return
|
||||
if status != "completed":
|
||||
update_generated_video(job_id, local_id, status="failed", error=f"video status: {status}", progress=progress)
|
||||
return
|
||||
|
||||
content = client.get(f"{base}/videos/{video_api_id}/content", headers=headers)
|
||||
content.raise_for_status()
|
||||
out_mp4.write_bytes(content.content)
|
||||
update_generated_video(
|
||||
job_id,
|
||||
local_id,
|
||||
status="completed",
|
||||
progress=100,
|
||||
url=f"/jobs/{job_id}/storyboard-videos/{local_id}.mp4",
|
||||
error="",
|
||||
)
|
||||
download_generated_video(client, base, headers, video_api_id, direct_url, out_mp4)
|
||||
update_generated_video(
|
||||
job_id,
|
||||
local_id,
|
||||
status="completed",
|
||||
progress=100,
|
||||
url=f"/jobs/{job_id}/storyboard-videos/{local_id}.mp4",
|
||||
error="",
|
||||
)
|
||||
except Exception as e:
|
||||
update_generated_video(job_id, local_id, status="failed", error=str(e)[:500])
|
||||
update_generated_video(job_id, local_id, status="failed", error=str(e)[:500])
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/frames/{idx}/storyboard/video", response_model=Job)
|
||||
@@ -1666,7 +1723,7 @@ def generate_storyboard_video(job_id: str, idx: int, req: GenerateStoryboardVide
|
||||
poster = storyboard_ref_url(job_id, ref) or f"/jobs/{job_id}/frames/{idx}.jpg"
|
||||
|
||||
local_id = uuid.uuid4().hex[:12]
|
||||
model = req.model.strip() or VIDEO_MODEL
|
||||
model = resolve_video_model(req.model)
|
||||
seconds = video_seconds(float(req.duration or 4))
|
||||
item = GeneratedVideo(
|
||||
id=local_id,
|
||||
@@ -1686,6 +1743,14 @@ def generate_storyboard_video(job_id: str, idx: int, req: GenerateStoryboardVide
|
||||
return job
|
||||
|
||||
|
||||
@app.get("/jobs/{job_id}/storyboard-videos/{video_id}.mp4")
|
||||
def get_storyboard_video(job_id: str, video_id: str):
|
||||
p = job_dir(job_id) / "storyboard_videos" / video_id / "video.mp4"
|
||||
if not p.exists():
|
||||
raise HTTPException(404, "storyboard video not found")
|
||||
return FileResponse(p, media_type="video/mp4")
|
||||
|
||||
|
||||
@app.put("/jobs/{job_id}/frames/{idx}/storyboard", response_model=Job)
|
||||
def update_storyboard(job_id: str, idx: int, req: UpdateStoryboardReq) -> Job:
|
||||
"""更新分镜的编排字段(subject / product / scene / action / duration / reference_ids)"""
|
||||
|
||||
Reference in New Issue
Block a user