diff --git a/api/main.py b/api/main.py
index ed5a485..5c2fadb 100644
--- a/api/main.py
+++ b/api/main.py
@@ -4284,16 +4284,50 @@ def fallback_product_view(index: int) -> dict:
}
+def parse_product_view_response(raw: str, index: int) -> dict:
+ text = (raw or "").strip()
+ text = re.sub(r"^```(?:json)?\s*", "", text, flags=re.I).strip()
+ text = re.sub(r"\s*```$", "", text).strip()
+ match = re.search(r"\{[\s\S]*\}", text)
+ json_text = match.group(0) if match else text
+ try:
+ data = json.loads(json_text)
+ except Exception:
+ view_match = re.search(r'["\']?view["\']?\s*[::]\s*["\']?([a-z0-9_]+)', text, flags=re.I)
+ note_match = re.search(
+ r'["\']?note["\']?\s*[::]\s*["\']?([\s\S]*?)(?:["\']?\s*,\s*["\']?confidence|["\']?\s*[,}]\s*$)',
+ text,
+ flags=re.I,
+ )
+ confidence_match = re.search(r'["\']?confidence["\']?\s*[::]\s*["\']?([0-9.]+)', text, flags=re.I)
+ data = {
+ "view": view_match.group(1) if view_match else "",
+ "note": note_match.group(1) if note_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)
+ note = str(data.get("note") or "").strip().strip('"\' ,,。')
+ note = re.sub(r"\s+", " ", note)[:220] 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}
+
+
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. "
+ "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. "
"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}."
+ "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}."
)
try:
resp = llm().chat.completions.create(
@@ -4309,17 +4343,7 @@ def analyze_product_view(ref_path: Path, index: int) -> dict:
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}
+ return parse_product_view_response(raw, index)
except Exception as e:
fallback = fallback_product_view(index)
fallback["note"] = f"{fallback['note']} 识别失败:{str(e)[:80]}"
diff --git a/docs/source-analysis.html b/docs/source-analysis.html
index c41d9b1..3949732 100644
--- a/docs/source-analysis.html
+++ b/docs/source-analysis.html
@@ -950,6 +950,18 @@ SubjectAsset {
变更记录
这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。
+
+
+ 2026-05-17 · 产品视角识别容错解析
+ API
+ Workflow
+
+
+
问题:视觉模型有时能判断视角,但返回的 JSON 含换行、引号或尾部格式问题,后端直接 json.loads 会失败,导致整张图被标成“识别失败”。
+
改动:api/main.py 新增 parse_product_view_response:先按严格 JSON 解析,失败后从原始输出里容错提取 view、note 和 confidence。同时收紧产品视角识别 prompt,要求模型输出单行 JSON。
+
影响:POST /jobs/{id}/assets/product-views/analyze 在模型输出不完全规范时也能保留视角结果,减少无意义 fallback;真正无法识别时才按上传顺序兜底。
+
+
2026-05-17 · 产品素材池取消数量上限,单条生成自动选图