From 2472fb2644900d6b03623846bb0f6ea499b6600f Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 09:54:21 +0800 Subject: [PATCH] auto-save 2026-05-13 09:54 (~3) --- .memory/worklog.json | 7 +++ api/main.py | 90 +++++++++++++++++++++++-------------- web/components/lightbox.tsx | 44 ++---------------- 3 files changed, 67 insertions(+), 74 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 2b73c1e..ad382a1 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1157,6 +1157,13 @@ "type": "session-heartbeat", "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 09:42 (~2)", "files_changed": 1 + }, + { + "ts": "2026-05-13T09:48:41+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 09:48 (~1)", + "hash": "e012c07", + "files_changed": 1 } ] } diff --git a/api/main.py b/api/main.py index d0fa9e8..e766af0 100644 --- a/api/main.py +++ b/api/main.py @@ -660,7 +660,7 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job: if req.extra_prompt.strip(): full_prompt = f"{full_prompt}. Include: {req.extra_prompt.strip()}" if req.negative_prompt.strip(): - full_prompt = f"{full_prompt}. Do NOT include: {req.negative_prompt.strip()}. Output must be clean without any watermark, username text, or platform logo." + full_prompt = f"{full_prompt}. Avoid: {req.negative_prompt.strip()}" if not full_prompt: raise HTTPException(400, "prompt required") @@ -669,42 +669,66 @@ def generate_image(job_id: str, idx: int, req: GenerateReq) -> Job: import base64 as b64lib import time as _time + import httpx - try: - if req.mode == "edit": - # image-to-image:用 generations 端点带 image 参数 - img_b64 = b64lib.b64encode(frame_path.read_bytes()).decode("ascii") - data_uri = f"data:image/jpeg;base64,{img_b64}" - # OpenAI SDK 不直接支持 image 参数,用底层 httpx - import httpx - with httpx.Client(timeout=120) as client: - r = client.post( - f"{LLM_BASE_URL}/images/generations", - headers={ - "Authorization": f"Bearer {LLM_API_KEY}", - "Content-Type": "application/json", - }, - json={ - "model": model, - "prompt": full_prompt, - "image": data_uri, - "n": 1, - }, - ) - r.raise_for_status() - resp_data = r.json() - else: - # text-only - resp = llm().images.generate(model=model, prompt=full_prompt, n=1) - resp_data = resp.model_dump() if hasattr(resp, "model_dump") else {"data": [{"b64_json": resp.data[0].b64_json}]} - except httpx.HTTPStatusError as e: - raise HTTPException(500, f"image gen HTTP {e.response.status_code}: {e.response.text[:300]}") - except Exception as e: - raise HTTPException(500, f"image gen failed: {e}") + img_b64: str | None = None + if req.mode == "edit": + img_b64 = b64lib.b64encode(frame_path.read_bytes()).decode("ascii") + + MAX_ATTEMPTS = 3 + resp_data: dict = {} + last_err = "" + for attempt in range(MAX_ATTEMPTS): + try: + if req.mode == "edit": + data_uri = f"data:image/jpeg;base64,{img_b64}" + # OpenAI SDK 不直接支持 image 参数,用底层 httpx + with httpx.Client(timeout=120) as client: + r = client.post( + f"{LLM_BASE_URL}/images/generations", + headers={ + "Authorization": f"Bearer {LLM_API_KEY}", + "Content-Type": "application/json", + }, + json={ + "model": model, + "prompt": full_prompt, + "image": data_uri, + "n": 1, + }, + ) + r.raise_for_status() + resp_data = r.json() + else: + # text-only + resp = llm().images.generate(model=model, prompt=full_prompt, n=1) + resp_data = resp.model_dump() if hasattr(resp, "model_dump") else {"data": [{"b64_json": resp.data[0].b64_json}]} + + if resp_data.get("data"): + break + err_obj = resp_data.get("error") or {} + last_err = f"empty data · {err_obj.get('code', '')} · {str(err_obj.get('message', ''))[:200]}" + except httpx.HTTPStatusError as e: + body = e.response.text + transient = ( + e.response.status_code >= 500 + or "incomplete_generation" in body + or "rate_limit" in body + or "timeout" in body.lower() + ) + last_err = f"HTTP {e.response.status_code}: {body[:200]}" + if not transient: + raise HTTPException(500, f"image gen HTTP {e.response.status_code}: {body[:300]}") + 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) + _time.sleep(1.5 * (attempt + 1)) data_arr = resp_data.get("data", []) if not data_arr: - raise HTTPException(500, "image gen returned no data") + raise HTTPException(500, f"image gen failed after {MAX_ATTEMPTS} attempts: {last_err}") item = data_arr[0] b64 = item.get("b64_json") diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index 8c9f63c..69eb4b0 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react" import { createPortal } from "react-dom" import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, RefreshCw, Plus } from "lucide-react" -import { frameUrl, describeFrame, translateText, generateImage, generatedImageUrl, type KeyFrame, type Job } from "@/lib/api" +import { frameUrl, describeFrame, translateText, generateImage, type KeyFrame, type Job } from "@/lib/api" import { toast } from "sonner" type CustomItem = { id: string; zh: string; en: string; translating: boolean } @@ -96,12 +96,12 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const updated = await generateImage(jobId, f.index, { prompt: base, extra_prompt: extraEn, - negative_prompt: "水印, @用户名, TikTok logo, 平台文字, 浮水印", + negative_prompt: "watermark, username text, social media handle, platform logo, overlay text, captions", model: "gemini-3-pro-image-preview", mode: "edit", }) onJobUpdate?.(updated) - toast.success(`分镜 ${f.index + 1} 垫图生成完成 · 已加入生图卡`) + toast.success(`分镜 ${f.index + 1} 垫图生成完成 → 「生图」节点查看`) } catch (e) { toast.error("生图失败:" + (e instanceof Error ? e.message : String(e))) } finally { @@ -356,44 +356,6 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o )} - {/* 已生成的垫图(与生图卡同源) */} - {f.generated_images && f.generated_images.length > 0 && ( -
-
-
- - 已生成垫图 - · {f.generated_images.length} -
- 同步到 →「生图」节点 -
-
- {f.generated_images.map((g) => ( - - {`gen - {g.selected && ( -
- -
- )} -
- ))} -
-
- )}