diff --git a/.memory/worklog.json b/.memory/worklog.json index c32f11e..134f41d 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,131 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "d3ee267", - "message": "auto-save 2026-05-15 10:31 (~1)", - "ts": "2026-05-15T10:31:58+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 10:31 (~1)", - "ts": "2026-05-15T02:33:08Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "919cf8f", - "message": "auto-save 2026-05-15 10:37 (~1)", - "ts": "2026-05-15T10:37:36+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 10:37 (~1)", - "ts": "2026-05-15T02:43:08Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "48007a7", - "message": "auto-save 2026-05-15 10:43 (~1)", - "ts": "2026-05-15T10:43:15+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "d58c710", - "message": "auto-save 2026-05-15 10:48 (~1)", - "ts": "2026-05-15T10:48:49+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 10:48 (~1)", - "ts": "2026-05-15T02:53:08Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "0debed8", - "message": "auto-save 2026-05-15 10:54 (~1)", - "ts": "2026-05-15T10:54:23+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "6b593cc", - "message": "auto-save 2026-05-15 11:00 (~1)", - "ts": "2026-05-15T11:01:11+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 11:00 (~1)", - "ts": "2026-05-15T03:04:44Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "9e5d853", - "message": "auto-save 2026-05-15 11:07 (~1)", - "ts": "2026-05-15T11:07:11+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "4d66653", - "message": "auto-save 2026-05-15 11:12 (~1)", - "ts": "2026-05-15T11:12:45+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 11:12 (~1)", - "ts": "2026-05-15T03:14:44Z", - "type": "session-heartbeat" - }, - { - "files_changed": 6, - "hash": "08aed2a", - "message": "auto-save 2026-05-15 11:18 (+1, ~4)", - "ts": "2026-05-15T11:18:18+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "e6b2768", - "message": "auto-save 2026-05-15 11:23 (~1)", - "ts": "2026-05-15T11:23:51+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 11:23 (~1)", - "ts": "2026-05-15T03:24:44Z", - "type": "session-heartbeat" - }, - { - "files_changed": 5, - "hash": "55a9a9d", - "message": "auto-save 2026-05-15 11:29 (+1, ~3)", - "ts": "2026-05-15T11:29:23+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 11:29 (+1, ~3)", - "ts": "2026-05-15T03:34:44Z", - "type": "session-heartbeat" - }, - { - "files_changed": 1, - "hash": "967a827", - "message": "auto-save 2026-05-15 11:34 (~1)", - "ts": "2026-05-15T11:34:55+08:00", - "type": "commit" - }, { "files_changed": 1, "hash": "7bf9e0f", @@ -3273,6 +3147,123 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-17 16:43 (~4)", "files_changed": 1 + }, + { + "ts": "2026-05-17T16:54:22+08:00", + "type": "commit", + "message": "auto-save 2026-05-17 16:54 (~4)", + "hash": "9798e97", + "files_changed": 4 + }, + { + "ts": "2026-05-17T16:56:40+08:00", + "type": "commit", + "message": "feat: optimize product pool uploads", + "hash": "84108ee", + "files_changed": 1 + }, + { + "ts": "2026-05-17T08:58:27Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T09:08:27Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T09:18:27Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T09:28:27Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T09:38:27Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T09:48:27Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T09:58:27Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T10:08:27Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T10:18:27Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T10:28:28Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T10:38:28Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T10:48:28Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T10:58:28Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T11:08:28Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T11:18:28Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:feat: optimize product pool uploads", + "files_changed": 1 + }, + { + "ts": "2026-05-17T19:24:23+08:00", + "type": "commit", + "message": "fix: tolerate product view model output", + "hash": "a9d5962", + "files_changed": 2 + }, + { + "ts": "2026-05-17T11:28:28Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:fix: tolerate product view model output", + "files_changed": 1 } ] } diff --git a/api/main.py b/api/main.py index 5c2fadb..1ae7603 100644 --- a/api/main.py +++ b/api/main.py @@ -4274,12 +4274,54 @@ PRODUCT_VIEW_LABELS = { "back_bottom": "背面/底部", } +PRODUCT_BACKGROUND_VALUES = ["white", "black", "simple", "complex", "unknown"] +PRODUCT_USE_TAG_VALUES = [ + "hero_packshot", + "wearing_scale", + "inner_contact", + "side_thickness", + "asymmetry", + "button_detail", + "back_bottom", + "material_texture", +] + + +def default_product_use_tags(view: str) -> list[str]: + defaults = { + "front": ["hero_packshot", "asymmetry"], + "left_45": ["hero_packshot", "asymmetry", "button_detail"], + "right_45": ["hero_packshot", "asymmetry", "button_detail"], + "side_thickness": ["side_thickness", "wearing_scale"], + "inner_contacts": ["inner_contact", "wearing_scale"], + "back_bottom": ["back_bottom", "material_texture"], + } + return defaults.get(view, ["hero_packshot"]) + + +def normalize_product_use_tags(tags: object, view: str) -> list[str]: + if isinstance(tags, str): + raw_tags = re.split(r"[,,/、\s]+", tags) + elif isinstance(tags, list): + raw_tags = [str(x) for x in tags] + else: + raw_tags = [] + result = [] + for tag in raw_tags + default_product_use_tags(view): + tag = str(tag).strip() + if tag in PRODUCT_USE_TAG_VALUES and tag not in result: + result.append(tag) + return result[:4] + def fallback_product_view(index: int) -> dict: view = PRODUCT_VIEW_VALUES[min(index, len(PRODUCT_VIEW_VALUES) - 1)] return { "view": view, + "background": "unknown", + "use_tags": default_product_use_tags(view), "note": f"{PRODUCT_VIEW_LABELS.get(view, view)}参考;模型识别不可用时按上传顺序自动标注,请人工只检查备注。", + "risk": "模型识别不可用,按上传顺序兜底", "confidence": 0.25, } @@ -4300,21 +4342,37 @@ def parse_product_view_response(raw: str, index: int) -> dict: flags=re.I, ) confidence_match = re.search(r'["\']?confidence["\']?\s*[::]\s*["\']?([0-9.]+)', text, flags=re.I) + background_match = re.search(r'["\']?background["\']?\s*[::]\s*["\']?([a-z0-9_]+)', text, flags=re.I) + tags_match = re.search(r'["\']?use_tags["\']?\s*[::]\s*\[([\s\S]*?)\]', text, flags=re.I) + risk_match = re.search( + r'["\']?risk["\']?\s*[::]\s*["\']?([\s\S]*?)(?:["\']?\s*[,}]\s*$)', + text, + flags=re.I, + ) data = { "view": view_match.group(1) if view_match else "", + "background": background_match.group(1) if background_match else "unknown", + "use_tags": re.findall(r"[a-z_]+", tags_match.group(1)) if tags_match else [], "note": note_match.group(1) if note_match else "", + "risk": risk_match.group(1) if risk_match else "", "confidence": confidence_match.group(1) if confidence_match else 0.45, } view = str(data.get("view") or "").strip().strip('"\' ,。') if view not in PRODUCT_VIEW_VALUES: return fallback_product_view(index) + background = str(data.get("background") or "unknown").strip().strip('"\' ,。') + if background not in PRODUCT_BACKGROUND_VALUES: + background = "unknown" + use_tags = normalize_product_use_tags(data.get("use_tags"), view) note = str(data.get("note") or "").strip().strip('"\' ,,。') note = re.sub(r"\s+", " ", note)[:220] or f"{PRODUCT_VIEW_LABELS.get(view, view)}参考" + risk = str(data.get("risk") or "").strip().strip('"\' ,,。') + risk = re.sub(r"\s+", " ", risk)[:120] try: confidence = max(0.0, min(1.0, float(data.get("confidence", 0.5)))) except Exception: confidence = 0.5 - return {"view": view, "note": note, "confidence": confidence} + return {"view": view, "background": background, "use_tags": use_tags, "note": note, "risk": risk, "confidence": confidence} def analyze_product_view(ref_path: Path, index: int) -> dict: @@ -4324,10 +4382,13 @@ def analyze_product_view(ref_path: Path, index: int) -> dict: prompt = ( "You are inspecting a product reference image for a SKG neck-and-shoulder wearable massage device. The background may be white, black, or simple studio color. " "Classify the camera/view angle into exactly one enum: front, left_45, right_45, side_thickness, inner_contacts, back_bottom. " - "Also write a concise Chinese note for video generation, focused on visible structure, asymmetry, thickness, inner massage contacts, buttons, opening width, and shoulder-neck wearing scale. " + "Classify background into exactly one enum: white, black, simple, complex, unknown. Do not request or perform background conversion. " + "Add use_tags from this enum only: hero_packshot, wearing_scale, inner_contact, side_thickness, asymmetry, button_detail, back_bottom, material_texture. " + "Also write a concise Chinese note for video generation, focused on visible structure, asymmetry, thickness, inner massage contacts, buttons, opening width, shoulder-neck wearing scale, and what this image is best used for in video generation. " + "Write risk in Chinese only if this reference may mislead video generation, such as cropped product, hand blocking, low detail, strong reflection, or background confusion; otherwise use empty string. " "If uncertain, choose the closest useful view; do not ask the user. " "Output one-line strict JSON only. Do not use markdown or line breaks. " - "{\"view\":\"front|left_45|right_45|side_thickness|inner_contacts|back_bottom\", \"note\":\"中文备注\", \"confidence\":0.0}." + "{\"view\":\"front|left_45|right_45|side_thickness|inner_contacts|back_bottom\", \"background\":\"white|black|simple|complex|unknown\", \"use_tags\":[\"hero_packshot\"], \"note\":\"中文备注\", \"risk\":\"\", \"confidence\":0.0}." ) try: resp = llm().chat.completions.create( @@ -4364,7 +4425,10 @@ def analyze_product_views(job_id: str, req: AnalyzeProductViewsReq) -> dict: items.append({ "index": index, "view": result["view"], + "background": result.get("background", "unknown"), + "use_tags": result.get("use_tags", default_product_use_tags(result["view"])), "note": result["note"], + "risk": result.get("risk", ""), "confidence": result["confidence"], }) used = {item["view"] for item in items} diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index 1b6f7de..d265426 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -89,7 +89,10 @@ type ProductRefItem = { id: string ref: ImageRef view: string + background: string + useTags: string[] note: string + risk: string source: "upload" | "ai" confidence?: number } @@ -105,6 +108,25 @@ const PRODUCT_VIEW_SLOTS = [ const MAX_PRODUCT_REFS_PER_VIDEO = 6 +const PRODUCT_BACKGROUND_LABELS: Record = { + white: "白底", + black: "黑底", + simple: "纯色/简单", + complex: "复杂背景", + unknown: "背景未知", +} + +const PRODUCT_USE_TAG_LABELS: Record = { + hero_packshot: "主外观", + wearing_scale: "佩戴比例", + inner_contact: "触点", + side_thickness: "厚度", + asymmetry: "非对称", + button_detail: "按键", + back_bottom: "背底", + material_texture: "材质", +} + const controlClass = "h-10 rounded-md border border-white/10 bg-black/55 px-3 text-[12px] text-white outline-none transition focus:border-cyan-300/60 disabled:cursor-not-allowed disabled:opacity-40" @@ -330,12 +352,39 @@ function productViewLabel(view: string) { return PRODUCT_VIEW_SLOTS.find((slot) => slot.value === view)?.label ?? view } +function productBackgroundLabel(background: string) { + return PRODUCT_BACKGROUND_LABELS[background] ?? PRODUCT_BACKGROUND_LABELS.unknown +} + +function defaultProductUseTags(view: string) { + const defaults: Record = { + front: ["hero_packshot", "asymmetry"], + left_45: ["hero_packshot", "asymmetry", "button_detail"], + right_45: ["hero_packshot", "asymmetry", "button_detail"], + side_thickness: ["side_thickness", "wearing_scale"], + inner_contacts: ["inner_contact", "wearing_scale"], + back_bottom: ["back_bottom", "material_texture"], + } + return defaults[view] ?? ["hero_packshot"] +} + +function normalizeProductUseTags(tags: string[] | undefined, view: string) { + const result: string[] = [] + for (const tag of [...(tags ?? []), ...defaultProductUseTags(view)]) { + if (PRODUCT_USE_TAG_LABELS[tag] && !result.includes(tag)) result.push(tag) + } + return result.slice(0, 4) +} + function createProductRefItem( ref: ImageRef, index: number, source: ProductRefItem["source"] = "upload", view?: string, note?: string, + background = "unknown", + useTags?: string[], + risk = "", confidence?: number, ): ProductRefItem { const slot = PRODUCT_VIEW_SLOTS[index] ?? PRODUCT_VIEW_SLOTS[PRODUCT_VIEW_SLOTS.length - 1] @@ -344,7 +393,10 @@ function createProductRefItem( id: productRefKey(ref, index), ref, view: view ?? targetSlot.value, + background, + useTags: normalizeProductUseTags(useTags, view ?? targetSlot.value), note: note ?? targetSlot.hint, + risk, source, confidence, } @@ -353,7 +405,11 @@ function createProductRefItem( function productReferenceNotes(items: ProductRefItem[]) { if (!items.length) return "" return items - .map((item, index) => `${index + 1}. ${productViewLabel(item.view)}:${item.note || "无补充备注"}`) + .map((item, index) => { + const tags = item.useTags.map((tag) => PRODUCT_USE_TAG_LABELS[tag]).filter(Boolean).join("/") + const risk = item.risk ? `;风险:${item.risk}` : "" + return `${index + 1}. ${productViewLabel(item.view)}|${productBackgroundLabel(item.background)}|${tags}:${item.note || "无补充备注"}${risk}` + }) .join(";") } diff --git a/web/lib/api.ts b/web/lib/api.ts index 2aae1e4..236b99a 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -166,7 +166,10 @@ export async function generateProductAngleAsset( export interface ProductViewAnalysisItem { index: number view: string + background?: string + use_tags?: string[] note: string + risk?: string confidence: number }