From ea31219ed7f35a2d1f319148618f6f07eec6f1e6 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 09:59:55 +0800 Subject: [PATCH] auto-save 2026-05-13 09:59 (~4) --- .memory/worklog.json | 13 +++++++++++++ api/main.py | 35 +++++++++++++++++++++++++++-------- web/components/lightbox.tsx | 24 +++++++++++++----------- web/lib/api.ts | 2 +- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index ad382a1..6b85ff0 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1164,6 +1164,19 @@ "message": "auto-save 2026-05-13 09:48 (~1)", "hash": "e012c07", "files_changed": 1 + }, + { + "ts": "2026-05-13T09:54:21+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 09:54 (~3)", + "hash": "2472fb2", + "files_changed": 3 + }, + { + "ts": "2026-05-13T01:57:36Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-13 09:54 (~3)", + "files_changed": 2 } ] } diff --git a/api/main.py b/api/main.py index e766af0..7dd7ef4 100644 --- a/api/main.py +++ b/api/main.py @@ -641,6 +641,7 @@ class GenerateReq(BaseModel): negative_prompt: str = "" # ✗ 不需要的元素(负向) model: str = "" # 留空用 IMAGE_MODEL 默认 mode: str = "edit" # "edit" 带参考图,"text" 纯文字 + from_selected: bool = False # True 时优先用 frame.selected 的生成图作 reference(迭代),否则原关键帧 @app.post("/jobs/{job_id}/frames/{idx}/generate", response_model=Job) @@ -656,6 +657,17 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job: if not frame_path.exists(): raise HTTPException(404, "frame file missing") + # 决定 i2i 参考图:from_selected=True 且存在 selected 生成图 → 用它(迭代);否则原关键帧 + reference_path = frame_path + reference_source = "keyframe" + if req.from_selected: + sel = next((g for g in frame.generated_images if g.selected), None) + if sel: + sel_path = job_dir(job_id) / "gen" / f"{idx:03d}_{sel.id}.jpg" + if sel_path.exists(): + reference_path = sel_path + reference_source = f"gen:{sel.id[:6]}" + full_prompt = req.prompt.strip() if req.extra_prompt.strip(): full_prompt = f"{full_prompt}. Include: {req.extra_prompt.strip()}" @@ -673,14 +685,18 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job: img_b64: str | None = None if req.mode == "edit": - img_b64 = b64lib.b64encode(frame_path.read_bytes()).decode("ascii") + img_b64 = b64lib.b64encode(reference_path.read_bytes()).decode("ascii") - MAX_ATTEMPTS = 3 + # 尝试 i2i 最多 3 次,全失败时降级 text-only 再试 1 次 + plan: list[str] = ([req.mode] * 3) if req.mode == "edit" else [req.mode] + if req.mode == "edit": + plan.append("text") # i2i 都失败时自动降级 resp_data: dict = {} last_err = "" - for attempt in range(MAX_ATTEMPTS): + effective_mode = req.mode + for attempt, current_mode in enumerate(plan): try: - if req.mode == "edit": + if current_mode == "edit": data_uri = f"data:image/jpeg;base64,{img_b64}" # OpenAI SDK 不直接支持 image 参数,用底层 httpx with httpx.Client(timeout=120) as client: @@ -705,6 +721,7 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job: resp_data = resp.model_dump() if hasattr(resp, "model_dump") else {"data": [{"b64_json": resp.data[0].b64_json}]} if resp_data.get("data"): + effective_mode = current_mode break err_obj = resp_data.get("error") or {} last_err = f"empty data · {err_obj.get('code', '')} · {str(err_obj.get('message', ''))[:200]}" @@ -722,13 +739,15 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job: except Exception as e: last_err = f"{type(e).__name__}: {e}" - if attempt < MAX_ATTEMPTS - 1: - print(f"[image gen retry {attempt + 1}/{MAX_ATTEMPTS}] {last_err}", flush=True) + if attempt < len(plan) - 1: + next_mode = plan[attempt + 1] + tag = f"fallback → {next_mode}" if next_mode != current_mode else f"retry {attempt + 1}/{len(plan)}" + print(f"[image gen {tag}] {last_err}", flush=True) _time.sleep(1.5 * (attempt + 1)) data_arr = resp_data.get("data", []) if not data_arr: - raise HTTPException(500, f"image gen failed after {MAX_ATTEMPTS} attempts: {last_err}") + raise HTTPException(500, f"image gen failed after {len(plan)} attempts: {last_err}") item = data_arr[0] b64 = item.get("b64_json") @@ -745,7 +764,7 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job: id=gen_id, prompt=full_prompt, model=model, - mode=req.mode, + mode=effective_mode, url=f"/jobs/{job_id}/frames/{idx}/gen/{gen_id}.jpg", selected=False, created_at=_time.time(), diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index 69eb4b0..da6bf91 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -80,14 +80,13 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const isSelected = selected.has(f.index) const desc = f.description - const handleGenerateMat = async () => { + const handleGenerateNext = async () => { if (activeIndex === null || !f) return const base = f.description?.suggested_prompt?.trim() if (!base) { toast.error("请先识别此分镜(右上角『识别』按钮)") return } - // 自动选用此帧 → ImageGenCard 才会渲染 if (!selected.has(f.index)) onToggleSelect(f.index) const extraEn = customs.filter((c) => c.en).map((c) => c.en).join(", ") @@ -99,9 +98,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o negative_prompt: "watermark, username text, social media handle, platform logo, overlay text, captions", model: "gemini-3-pro-image-preview", mode: "edit", + from_selected: true, // 优先用上一轮 selected 的生成图作 reference(迭代) }) onJobUpdate?.(updated) - toast.success(`分镜 ${f.index + 1} 垫图生成完成 → 「生图」节点查看`) + toast.success(`分镜 ${f.index + 1} 生成完成 → 「生图」节点查看`) } catch (e) { toast.error("生图失败:" + (e instanceof Error ? e.message : String(e))) } finally { @@ -340,19 +340,21 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o - {!desc?.suggested_prompt && ( -
先识别此分镜,再生成垫图
- )} - {desc?.suggested_prompt && customs.length === 0 && ( -
未加自定义元素 · 将仅按识别结果生成
+ {!desc?.suggested_prompt ? ( +
先识别此分镜,再生成
+ ) : ( +
+ 基于:{f.generated_images?.some((g) => g.selected) ? "上一轮已选生成图" : "原关键帧"} + {customs.length > 0 && ` + ${customs.length} 条提取元素`} +
)} diff --git a/web/lib/api.ts b/web/lib/api.ts index effb754..f2e3773 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -145,7 +145,7 @@ export async function translateText(text: string, target: "en" | "zh" = "en"): P export async function generateImage( jobId: string, frameIdx: number, - body: { prompt: string; extra_prompt?: string; negative_prompt?: string; model?: string; mode?: "edit" | "text" }, + body: { prompt: string; extra_prompt?: string; negative_prompt?: string; model?: string; mode?: "edit" | "text"; from_selected?: boolean }, ): Promise { const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/generate`, { method: "POST",