From 9600bb4925a19b6ba4dc746949ed26195435fe02 Mon Sep 17 00:00:00 2001 From: kang Date: Sun, 17 May 2026 16:38:02 +0800 Subject: [PATCH] auto-save 2026-05-17 16:37 (~2) --- .memory/worklog.json | 13 ++++--- api/main.py | 88 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index d40f5c2..d0e39af 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,11 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 10:09 (~1)", - "ts": "2026-05-15T02:13:07Z", - "type": "session-heartbeat" - }, { "files_changed": 1, "hash": "c3ee829", @@ -3273,6 +3267,13 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 2 项未提交变更 · 最近提交:auto-save 2026-05-17 16:27 (~4)", "files_changed": 2 + }, + { + "ts": "2026-05-17T16:32:41+08:00", + "type": "commit", + "message": "auto-save 2026-05-17 16:32 (~3)", + "hash": "2b0afee", + "files_changed": 3 } ] } diff --git a/api/main.py b/api/main.py index 690bc14..2c3203a 100644 --- a/api/main.py +++ b/api/main.py @@ -4202,6 +4202,10 @@ class GenerateProductAngleAssetReq(BaseModel): note: str = "" +class AnalyzeProductViewsReq(BaseModel): + refs: list[dict] = Field(default_factory=list) + + @app.get("/product-library/skg", response_model=list[ProductLibraryItem]) def list_skg_product_library() -> list[ProductLibraryItem]: """内置 SKG 白底产品图库。来源是本地筛选后的产品图 manifest。""" @@ -4260,6 +4264,90 @@ async def upload_storyboard_asset(job_id: str, file: UploadFile = File(...)) -> } +PRODUCT_VIEW_VALUES = ["front", "left_45", "right_45", "side_thickness", "inner_contacts", "back_bottom"] +PRODUCT_VIEW_LABELS = { + "front": "正面", + "left_45": "左 45", + "right_45": "右 45", + "side_thickness": "侧面厚度", + "inner_contacts": "内侧触点", + "back_bottom": "背面/底部", +} + + +def fallback_product_view(index: int) -> dict: + view = PRODUCT_VIEW_VALUES[min(index, len(PRODUCT_VIEW_VALUES) - 1)] + return { + "view": view, + "note": f"{PRODUCT_VIEW_LABELS.get(view, view)}参考;模型识别不可用时按上传顺序自动标注,请人工只检查备注。", + "confidence": 0.25, + } + + +def analyze_product_view(ref_path: Path, index: int) -> dict: + if not LLM_API_KEY: + return fallback_product_view(index) + img_b64 = base64.b64encode(ref_path.read_bytes()).decode("ascii") + prompt = ( + "You are inspecting a clean white-background product reference image for a SKG neck-and-shoulder wearable massage device. " + "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. " + "If uncertain, choose the closest useful view; do not ask the user. " + "Output strict JSON only: {\"view\":\"front|left_45|right_45|side_thickness|inner_contacts|back_bottom\", \"note\":\"中文备注\", \"confidence\":0.0}." + ) + try: + resp = llm().chat.completions.create( + model=VISION_MODEL, + messages=[{"role": "user", "content": [ + {"type": "text", "text": prompt}, + {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}, + ]}], + response_format={"type": "json_object"}, + temperature=0.1, + max_tokens=700, + ) + raw = (resp.choices[0].message.content or "").strip() + if not raw: + raw = (getattr(resp.choices[0].message, "reasoning_content", "") or "").strip() + match = re.search(r"\{[\s\S]*\}", raw) + data = json.loads(match.group(0) if match else raw) + view = str(data.get("view") or "").strip() + if view not in PRODUCT_VIEW_VALUES: + return fallback_product_view(index) + note = str(data.get("note") or "").strip() or f"{PRODUCT_VIEW_LABELS.get(view, view)}参考" + 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} + except Exception as e: + fallback = fallback_product_view(index) + fallback["note"] = f"{fallback['note']} 识别失败:{str(e)[:80]}" + return fallback + + +@app.post("/jobs/{job_id}/assets/product-views/analyze") +def analyze_product_views(job_id: str, req: AnalyzeProductViewsReq) -> dict: + if job_id not in JOBS: + raise HTTPException(404, "job not found") + items = [] + for index, ref in enumerate(req.refs[:6]): + ref_path = storyboard_ref_path(job_id, ref) + if not ref_path or not ref_path.exists(): + result = fallback_product_view(index) + else: + result = analyze_product_view(ref_path, index) + items.append({ + "index": index, + "view": result["view"], + "note": result["note"], + "confidence": result["confidence"], + }) + used = {item["view"] for item in items} + missing = [view for view in PRODUCT_VIEW_VALUES if view not in used] + return {"items": items, "missing_views": missing} + + @app.post("/jobs/{job_id}/assets/product-angle") def generate_product_angle_asset(job_id: str, req: GenerateProductAngleAssetReq) -> dict: if job_id not in JOBS: