From 2befdf4e40e3721383db96e4f4a22a69f7af11b5 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 21:24:32 +0800 Subject: [PATCH] auto-save 2026-05-13 21:24 (~6) --- .memory/worklog.json | 13 +++++++ api/main.py | 48 ++++++++++++++++++------- docs/source-analysis.html | 24 +++++++++++++ web/app/page.tsx | 3 ++ web/components/storyboard-workbench.tsx | 9 +++-- web/lib/api.ts | 1 + 6 files changed, 81 insertions(+), 17 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 7744a79..cdb0efb 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -2408,6 +2408,19 @@ "message": "auto-save 2026-05-13 21:13 (~3)", "hash": "a8b752b", "files_changed": 3 + }, + { + "ts": "2026-05-13T21:19:00+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 21:18 (~2)", + "hash": "d4eb18e", + "files_changed": 2 + }, + { + "ts": "2026-05-13T13:19:30Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 3 项未提交变更 · 最近提交:auto-save 2026-05-13 21:18 (~2)", + "files_changed": 3 } ] } diff --git a/api/main.py b/api/main.py index e563c2a..7a6ebd5 100644 --- a/api/main.py +++ b/api/main.py @@ -115,6 +115,11 @@ class GeneratedVideo(BaseModel): created_at: float = 0.0 +class VideoSourceRef(BaseModel): + kind: Literal["image", "source_video"] = "image" + url: str = "" + + class StoryboardScene(BaseModel): """分镜头编排:每个 selected 分镜对应一个 scene 描述 v2: 4 图槽 + 时长(复制粘贴模式)— 主体 / 场景 / 产品 / 动作 各一张图 @@ -265,6 +270,9 @@ def storyboard_ref_path(job_id: str, ref: dict | None) -> Path | None: except Exception: return None if kind == "keyframe": + clean = job_dir(job_id) / "cleaned" / f"{frame_idx:03d}.jpg" + if clean.exists(): + return clean p = job_dir(job_id) / "frames" / f"{frame_idx:03d}.jpg" return p if p.exists() else None if kind == "cutout": @@ -1643,6 +1651,7 @@ class GenerateStoryboardVideoReq(BaseModel): scene_image: dict | None = None product_image: dict | None = None action_image: dict | None = None + source_ref: VideoSourceRef | None = None model: str = "" size: str = "720x1280" @@ -1749,18 +1758,27 @@ def ark_reference_data_url(ref_img: Path) -> str: return f"data:{mime};base64,{base64.b64encode(ref_img.read_bytes()).decode('ascii')}" -def submit_video_create(client, url: str, headers: dict, ref_img: Path, payload: dict): +def submit_video_create(client, url: str, headers: dict, ref_img: Path, payload: dict, source_ref: VideoSourceRef | None = None): if video_uses_ark(): + content = [{"type": "text", "text": payload["prompt"]}] + if source_ref and source_ref.kind == "source_video" and source_ref.url: + content.append( + { + "type": "video_url", + "video_url": {"url": source_ref.url}, + "role": "reference_video", + } + ) + content.append( + { + "type": "image_url", + "image_url": {"url": ark_reference_data_url(ref_img)}, + "role": "first_frame", + } + ) data = { "model": payload["model"], - "content": [ - {"type": "text", "text": payload["prompt"]}, - { - "type": "image_url", - "image_url": {"url": ark_reference_data_url(ref_img)}, - "role": "first_frame", - }, - ], + "content": content, "ratio": size_to_video_ratio(str(payload.get("size", ""))), "duration": int(float(str(payload.get(VIDEO_DURATION_FIELD, 5)))), "watermark": False, @@ -1783,7 +1801,7 @@ def submit_video_create(client, url: str, headers: dict, ref_img: Path, payload: ) -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: +def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_path: Path, prompt: str, model: str, seconds: str, size: str, source_ref: VideoSourceRef | None = None) -> None: import httpx out_dir = job_dir(job_id) / "storyboard_videos" / local_id @@ -1801,7 +1819,10 @@ def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_pa 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) + resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, source_ref) + 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) if resp.status_code < 400: create = resp break @@ -1881,7 +1902,10 @@ def generate_storyboard_video(job_id: str, idx: int, req: GenerateStoryboardVide created_at=time.time(), ) update(job, generated_videos=[item] + job.generated_videos, message=f"视频生成已提交 · 分镜 {idx + 1}") - bg.add_task(render_storyboard_video, job_id, local_id, "", ref_path, prompt, model, seconds, req.size) + 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) return job diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 18a8786..8c27291 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -830,6 +830,30 @@ api/main.py

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-13 · 生视频携带原视频链接做节奏参考

+ StoryboardWorkbench + API +
+
+

问题:用户赶交付,希望直接把上传的原视频链接给视频模型参考,而不是只靠单张关键帧。

+

改动:前端提交生视频时增加 source_ref: { kind: "source_video", url: job.url };Ark 请求体在文本 prompt 和首帧之外追加 video_url 参考视频,用于模仿节奏、镜头运动和动作顺序。如果 Ark 返回 400/422 不接受参考视频字段,后端自动回退到“当前关键帧首帧生成”,保证这次不会直接阻断出片。

+

影响:web/app/page.tsxweb/lib/api.tsapi/main.pydocs/source-analysis.html

+
+
+
+
+

2026-05-13 · 快速出片改为关键帧直生视频

+ StoryboardWorkbench + Prompt +
+
+

问题:赶交付时不适合再让 4 图槽决定首帧;如果某个槽里是抠图元素,模型会拿碎元素当第一帧,视频容易不连贯。

+

改动:“生成视频”按钮改成直接用当前分镜关键帧作为首帧提交,4 图槽和改造目标只作为提示词参考;提示词强调一镜到底、首帧稳定、时间线连续、禁止跳切/换场景/主体变形。后端取关键帧时优先使用未应用的清洗版,否则使用当前 frame 文件。

+

影响:web/app/page.tsxweb/components/storyboard-workbench.tsxapi/main.py

+
+

2026-05-13 · 生视频支持火山方舟 Ark 异步任务

diff --git a/web/app/page.tsx b/web/app/page.tsx index a95c816..93d0bfd 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -250,6 +250,7 @@ export default function Home() { `竖屏 9:16,${duration.toFixed(1)} 秒,SKG 产品短视频广告。`, "直接根据当前分镜关键帧生成视频。必须使用输入的完整视频关键帧作为第一帧和视觉锚点:第一帧构图、主体位置、透视关系和光线方向保持稳定,然后从这一帧自然动起来。", "生成一段单镜头连续视频,一镜到底,不要跳切,不要突然换场景,不要突然换主体,不要蒙太奇,不要多镜头拼接。", + "如果提供了原视频链接,把它只作为节奏、镜头运动、动作顺序和画面调度参考;不要照搬原视频里的品牌、文字、水印、竞品产品或具体人物。", "时间线:0%-25% 保持首帧构图并轻微启动;25%-70% 做一个清晰、缓慢、可信的产品展示动作;70%-100% 镜头自然停稳在 SKG 产品或使用效果特写。", `主体改造:${subjectDirection}`, `产品替换:${productDirection}`, @@ -274,6 +275,7 @@ export default function Home() { frame_idx: frameIdx, label: `分镜 ${frameIdx + 1} 关键帧`, } + const sourceUrl = job.url?.trim() const updated = await generateStoryboardVideo(job.id, frameIdx, { prompt, duration, @@ -281,6 +283,7 @@ export default function Home() { scene_image: null, product_image: null, action_image: null, + source_ref: sourceUrl ? { kind: "source_video", url: sourceUrl } : null, model, size: "720x1280", }) diff --git a/web/components/storyboard-workbench.tsx b/web/components/storyboard-workbench.tsx index 969a967..1e85efd 100644 --- a/web/components/storyboard-workbench.tsx +++ b/web/components/storyboard-workbench.tsx @@ -122,7 +122,6 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU window.addEventListener("pointerup", onUp) } - const hasVideoRefs = !!(form.subject_image || form.scene_image || form.product_image || form.action_image) const currentModelLabel = VIDEO_MODELS.find((m) => m.value === videoModel)?.label ?? "Seedance" return ( @@ -303,7 +302,7 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
- 用当前 4 图槽、改造目标和时长提交生视频 API;生成中的进度和完成后的 MP4 会显示在 Video Gen 节点。 + 直接用当前分镜关键帧作为首帧快速出片;4 图槽和改造目标只作为提示词参考,生成中的进度和完成后的 MP4 会显示在 Video Gen 节点。
diff --git a/web/lib/api.ts b/web/lib/api.ts index c21f890..d0a94f1 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -382,6 +382,7 @@ export async function generateStoryboardVideo( scene_image?: ImageRef | null product_image?: ImageRef | null action_image?: ImageRef | null + source_ref?: { kind: "image" | "source_video"; url: string } | null model?: string size?: string },