auto-save 2026-05-13 21:40 (~6)
This commit is contained in:
93
api/main.py
93
api/main.py
@@ -127,6 +127,7 @@ class StoryboardScene(BaseModel):
|
||||
duration: float = 0
|
||||
first_image: dict | None = None
|
||||
last_image: dict | None = None
|
||||
product_images: list[dict] = Field(default_factory=list)
|
||||
# 4 图槽:dict 含 {kind, frame_idx, element_id?, cutout_id?, label}
|
||||
subject_image: dict | None = None
|
||||
scene_image: dict | None = None
|
||||
@@ -290,6 +291,12 @@ def storyboard_ref_path(job_id: str, ref: dict | None) -> Path | None:
|
||||
for p in candidates:
|
||||
if p.exists():
|
||||
return p
|
||||
if kind == "asset":
|
||||
asset_id = (ref.get("element_id") or ref.get("cutout_id") or "").strip()
|
||||
if not asset_id:
|
||||
return None
|
||||
p = job_dir(job_id) / "assets" / f"{asset_id}.jpg"
|
||||
return p if p.exists() else None
|
||||
return None
|
||||
|
||||
|
||||
@@ -306,6 +313,8 @@ def storyboard_ref_url(job_id: str, ref: dict | None) -> str:
|
||||
if cutout_id and cutout_id != element_id:
|
||||
return f"/jobs/{job_id}/frames/{int(frame_idx)}/elements/{element_id}/cutouts/{cutout_id}.jpg"
|
||||
return f"/jobs/{job_id}/frames/{int(frame_idx)}/elements/{element_id}/cutout.jpg"
|
||||
if kind == "asset" and ref.get("element_id"):
|
||||
return f"/jobs/{job_id}/assets/{ref.get('element_id')}.jpg"
|
||||
return ""
|
||||
|
||||
|
||||
@@ -1651,6 +1660,7 @@ class GenerateStoryboardVideoReq(BaseModel):
|
||||
duration: float = 4
|
||||
first_image: dict | None = None
|
||||
last_image: dict | None = None
|
||||
product_images: list[dict] = Field(default_factory=list)
|
||||
subject_image: dict | None = None
|
||||
scene_image: dict | None = None
|
||||
product_image: dict | None = None
|
||||
@@ -1770,7 +1780,7 @@ def submit_video_create(
|
||||
payload: dict,
|
||||
source_ref: VideoSourceRef | None = None,
|
||||
last_img: Path | None = None,
|
||||
product_img: Path | None = None,
|
||||
product_imgs: list[Path] | None = None,
|
||||
):
|
||||
if video_uses_ark():
|
||||
content = [{"type": "text", "text": payload["prompt"]}]
|
||||
@@ -1797,14 +1807,15 @@ def submit_video_create(
|
||||
"role": "last_frame",
|
||||
}
|
||||
)
|
||||
if product_img and product_img.exists():
|
||||
content.append(
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": ark_reference_data_url(product_img)},
|
||||
"role": "reference_image",
|
||||
}
|
||||
)
|
||||
for product_img in (product_imgs or [])[:6]:
|
||||
if product_img.exists():
|
||||
content.append(
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": ark_reference_data_url(product_img)},
|
||||
"role": "reference_image",
|
||||
}
|
||||
)
|
||||
data = {
|
||||
"model": payload["model"],
|
||||
"content": content,
|
||||
@@ -1841,14 +1852,13 @@ def render_storyboard_video(
|
||||
size: str,
|
||||
source_ref: VideoSourceRef | None = None,
|
||||
last_ref_path: Path | None = None,
|
||||
product_ref_path: Path | None = None,
|
||||
product_ref_paths: list[Path] | None = None,
|
||||
) -> None:
|
||||
import httpx
|
||||
|
||||
out_dir = job_dir(job_id) / "storyboard_videos" / local_id
|
||||
ref_img = out_dir / "reference.jpg"
|
||||
last_img = out_dir / "last_reference.jpg"
|
||||
product_img = out_dir / "product_reference.jpg"
|
||||
out_mp4 = out_dir / "video.mp4"
|
||||
base = video_api_base()
|
||||
headers = {"Authorization": f"Bearer {video_api_key()}"}
|
||||
@@ -1859,10 +1869,12 @@ def render_storyboard_video(
|
||||
if last_ref_path and last_ref_path.exists():
|
||||
prepare_video_reference(last_ref_path, last_img)
|
||||
prepared_last_img = last_img
|
||||
prepared_product_img: Path | None = None
|
||||
if product_ref_path and product_ref_path.exists():
|
||||
prepare_video_reference(product_ref_path, product_img)
|
||||
prepared_product_img = product_img
|
||||
prepared_product_imgs: list[Path] = []
|
||||
for i, product_ref_path in enumerate((product_ref_paths or [])[:6], start=1):
|
||||
if product_ref_path.exists():
|
||||
product_img = out_dir / f"product_reference_{i}.jpg"
|
||||
prepare_video_reference(product_ref_path, product_img)
|
||||
prepared_product_imgs.append(product_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}
|
||||
@@ -1870,14 +1882,14 @@ def render_storyboard_video(
|
||||
create = None
|
||||
create_errors: list[str] = []
|
||||
for create_path in VIDEO_CREATE_PATHS:
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, source_ref, prepared_last_img, prepared_product_img)
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, source_ref, prepared_last_img, prepared_product_imgs)
|
||||
if video_uses_ark() and source_ref and resp.status_code in {400, 422}:
|
||||
create_errors.append(f"{video_path(create_path)} + reference_video -> HTTP {resp.status_code}: {resp.text[:160]}")
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, prepared_product_img)
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, prepared_product_imgs)
|
||||
if video_uses_ark() and prepared_last_img and resp.status_code in {400, 422}:
|
||||
create_errors.append(f"{video_path(create_path)} + last_frame -> HTTP {resp.status_code}: {resp.text[:160]}")
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, None, prepared_product_img)
|
||||
if video_uses_ark() and prepared_product_img and resp.status_code in {400, 422}:
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, None, prepared_product_imgs)
|
||||
if video_uses_ark() and prepared_product_imgs and resp.status_code in {400, 422}:
|
||||
create_errors.append(f"{video_path(create_path)} + product_reference -> HTTP {resp.status_code}: {resp.text[:160]}")
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, None)
|
||||
if resp.status_code < 400:
|
||||
@@ -1942,7 +1954,8 @@ def generate_storyboard_video(job_id: str, idx: int, req: GenerateStoryboardVide
|
||||
raise HTTPException(404, "reference image missing")
|
||||
poster = storyboard_ref_url(job_id, ref) or f"/jobs/{job_id}/frames/{idx}.jpg"
|
||||
last_ref_path = storyboard_ref_path(job_id, req.last_image)
|
||||
product_ref_path = storyboard_ref_path(job_id, req.product_image)
|
||||
raw_product_refs = req.product_images[:6] if req.product_images else ([req.product_image] if req.product_image else [])
|
||||
product_ref_paths = [p for p in (storyboard_ref_path(job_id, r) for r in raw_product_refs) if p]
|
||||
|
||||
local_id = uuid.uuid4().hex[:12]
|
||||
model = resolve_video_model(req.model)
|
||||
@@ -1964,7 +1977,7 @@ def generate_storyboard_video(job_id: str, idx: int, req: GenerateStoryboardVide
|
||||
source_ref = req.source_ref
|
||||
if source_ref and source_ref.kind == "source_video" and not source_ref.url:
|
||||
source_ref = None
|
||||
bg.add_task(render_storyboard_video, job_id, local_id, "", ref_path, prompt, model, seconds, req.size, source_ref, last_ref_path, product_ref_path)
|
||||
bg.add_task(render_storyboard_video, job_id, local_id, "", ref_path, prompt, model, seconds, req.size, source_ref, last_ref_path, product_ref_paths)
|
||||
return job
|
||||
|
||||
|
||||
@@ -1976,6 +1989,44 @@ def get_storyboard_video(job_id: str, video_id: str):
|
||||
return FileResponse(p, media_type="video/mp4")
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/assets")
|
||||
async def upload_storyboard_asset(job_id: str, file: UploadFile = File(...)) -> dict:
|
||||
if job_id not in JOBS:
|
||||
raise HTTPException(404, "job not found")
|
||||
asset_id = uuid.uuid4().hex[:12]
|
||||
out_dir = job_dir(job_id) / "assets"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
tmp = out_dir / f"{asset_id}.upload"
|
||||
out = out_dir / f"{asset_id}.jpg"
|
||||
try:
|
||||
tmp.write_bytes(await file.read())
|
||||
img = Image.open(tmp).convert("RGB")
|
||||
img.thumbnail((1600, 1600), Image.Resampling.LANCZOS)
|
||||
img.save(out, "JPEG", quality=94)
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"product image upload failed: {e}")
|
||||
finally:
|
||||
try:
|
||||
tmp.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"kind": "asset",
|
||||
"frame_idx": -1,
|
||||
"element_id": asset_id,
|
||||
"cutout_id": asset_id,
|
||||
"label": file.filename or "SKG 产品图",
|
||||
}
|
||||
|
||||
|
||||
@app.get("/jobs/{job_id}/assets/{asset_id}.jpg")
|
||||
def get_storyboard_asset(job_id: str, asset_id: str):
|
||||
p = job_dir(job_id) / "assets" / f"{asset_id}.jpg"
|
||||
if not p.exists():
|
||||
raise HTTPException(404, "asset not found")
|
||||
return FileResponse(p, media_type="image/jpeg")
|
||||
|
||||
|
||||
@app.delete("/jobs/{job_id}/storyboard-videos/{video_id}", response_model=Job)
|
||||
def delete_storyboard_video(job_id: str, video_id: str) -> Job:
|
||||
"""删除 Video Gen 节点里的一个视频任务(成功/失败/排队都可删)。"""
|
||||
|
||||
Reference in New Issue
Block a user