From 839a3f6d4b4b2079978bb98b3ea9a81661e53dc8 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 09:37:15 +0800 Subject: [PATCH] auto-save 2026-05-13 09:37 (~4) --- .memory/worklog.json | 7 ++ api/main.py | 40 +++++++++++ web/components/lightbox.tsx | 134 +++++++++++++++++++++++++++++------- web/lib/api.ts | 14 ++++ 4 files changed, 169 insertions(+), 26 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index d7fd8ab..4dbc0ee 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1124,6 +1124,13 @@ "type": "session-heartbeat", "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 09:25 (~2)", "files_changed": 1 + }, + { + "ts": "2026-05-13T09:31:42+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 09:31 (~3)", + "hash": "fdc3162", + "files_changed": 3 } ] } diff --git a/api/main.py b/api/main.py index 72a47d0..d0fa9e8 100644 --- a/api/main.py +++ b/api/main.py @@ -462,6 +462,46 @@ class CreateJobReq(BaseModel): url: str +class TranslateReq(BaseModel): + text: str + target: Literal["en", "zh"] = "en" + + +@app.post("/translate") +def translate_text(req: TranslateReq) -> dict: + """单条文本翻译(给生图自定义提取元素 zh→en 用)""" + import re as _re + text = req.text.strip() + if not text: + return {"text": ""} + if not LLM_API_KEY: + raise HTTPException(503, "LLM_API_KEY 未配置") + + target_label = "English" if req.target == "en" else "Simplified Chinese" + prompt = ( + f"Translate the following text into concise {target_label}, suitable as an element " + "label in an image-generation prompt. Output only the translation itself — no quotes, " + "no punctuation, no explanation, no markdown.\n\n" + f"Input: {text}" + ) + try: + resp = llm().chat.completions.create( + model=TRANSLATE_MODEL, + messages=[{"role": "user", "content": prompt}], + temperature=0.2, + max_tokens=200, + ) + out = (resp.choices[0].message.content or "").strip() + if not out: + rc = getattr(resp.choices[0].message, "reasoning_content", "") or "" + if rc: + out = rc.strip().splitlines()[-1].strip() + out = _re.sub(r'^[\'"「『]+|[\'"」』]+$', "", out).strip() + return {"text": out} + except Exception as e: + raise HTTPException(500, f"translate failed: {e}") + + @app.get("/health") def health() -> dict: return { diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index 028e2d0..a1e8956 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -1,10 +1,12 @@ "use client" import { useEffect, useState } from "react" import { createPortal } from "react-dom" -import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, RefreshCw } from "lucide-react" -import { frameUrl, describeFrame, type KeyFrame, type Job } from "@/lib/api" +import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, RefreshCw, Plus } from "lucide-react" +import { frameUrl, describeFrame, translateText, type KeyFrame, type Job } from "@/lib/api" import { toast } from "sonner" +type CustomItem = { id: string; zh: string; en: string; translating: boolean } + interface Props { jobId: string frames: KeyFrame[] @@ -18,11 +20,38 @@ interface Props { } export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate, embedded = false }: Props) { - const [extractPrompt, setExtractPrompt] = useState("") const [describing, setDescribing] = useState(false) const [mounted, setMounted] = useState(false) + // 自定义提取元素 — 按 frame 隔离,切换 frame 后回到同一帧时还能看到之前加的 + const [customsByFrame, setCustomsByFrame] = useState>({}) + const [addInput, setAddInput] = useState("") useEffect(() => setMounted(true), []) + const customs = activeIndex !== null ? (customsByFrame[activeIndex] || []) : [] + const updateCustoms = (updater: (prev: CustomItem[]) => CustomItem[]) => { + if (activeIndex === null) return + const key = activeIndex + setCustomsByFrame((prev) => ({ ...prev, [key]: updater(prev[key] || []) })) + } + + const addCustom = async (zhRaw: string, presetEn?: string) => { + const zh = zhRaw.trim() + if (!zh || activeIndex === null) return + const id = Math.random().toString(36).slice(2, 8) + const skipTranslate = !!presetEn + updateCustoms((prev) => [...prev, { id, zh, en: presetEn || "", translating: !skipTranslate }]) + if (skipTranslate) return + try { + const en = await translateText(zh, "en") + updateCustoms((prev) => prev.map((c) => c.id === id ? { ...c, en, translating: false } : c)) + } catch (e) { + updateCustoms((prev) => prev.map((c) => c.id === id ? { ...c, en: "", translating: false } : c)) + toast.error("翻译失败:" + (e instanceof Error ? e.message : String(e))) + } + } + + const removeCustom = (id: string) => updateCustoms((prev) => prev.filter((c) => c.id !== id)) + useEffect(() => { if (activeIndex === null) return const onKey = (e: KeyboardEvent) => { @@ -185,23 +214,24 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o )} {desc.objects && desc.objects.length > 0 && ( -
-
- 物体(点击 → 自动填入提取框) +
+
+ 物体 · 点击加入自定义列表
-
+
{desc.objects.map((o, i) => ( ))} @@ -212,29 +242,81 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o )} - {/* 自定义提取 */} + {/* 自定义提取 — 多条 + 中英对照 */}
自定义提取元素 + {customs.length > 0 && ( + · {customs.length} + )}
-