""" app/services/ai_engine/benchmark_analyzer.py — 第2环 标杆8维分析引擎 复用 products.analyze_product_image 的 vision 范式,解耦成 service。 输入:爆款截图 bytes + 手填亮点;输出:8维配方 JSON(写入 benchmark_notes.features_json)。 key 链路遵基石B:plain_key 只活在调用方局部,本模块只收已构建的 clients。 """ import json import logging import re from app.services.ai_engine._benchmark_prompt import BENCHMARK_8DIM_PROMPT logger = logging.getLogger(__name__) # 8维字段顺序(与 prompt JSON key 一致;analyze_status 流转 done 时校验齐全) DIM_KEYS = [ "title_formula", "opening_hook", "content_structure", "selling_point_style", "emotion_tone", "target_audience", "topic_tags", "visual_rhythm", ] def parse_benchmark_json(raw: str) -> dict: """从模型返回文本提取 JSON(容错 markdown ```json 包裹)。""" cleaned = re.sub(r"```json\s*", "", raw) cleaned = re.sub(r"```\s*", "", cleaned) m = re.search(r"\{[\s\S]*\}", cleaned) if not m: raise ValueError("标杆分析返回中未找到 JSON") return json.loads(m.group()) def fallback_benchmark(highlights: str | None = None) -> dict: """vision 失败兜底:保证 8 维不空手,标 source=fallback 供前端提示。""" note = highlights or "截图未体现" return { "title_formula": note, "opening_hook": note, "content_structure": note, "selling_point_style": note, "emotion_tone": note, "target_audience": note, "topic_tags": note, "visual_rhythm": note, "confidence": 0.4, "source": "fallback", } async def analyze_benchmark(clients, screenshot: bytes | None, highlights: str | None) -> dict: """ 跑 8 维分析。截图走 vision;无截图但有手填亮点则把亮点拼进 prompt 兜底。 返回结构化 8 维 dict(含 confidence/source)。失败不抛,返回 fallback。 """ prompt = BENCHMARK_8DIM_PROMPT if highlights: prompt = f"{prompt}\n\n运营补充的亮点描述(作为分析参考):{highlights}" images = [screenshot] if screenshot else [] try: if not images: # 无截图:纯文本兜底(gpt_vision_analyze 支持空图走文本) logger.info("analyze_benchmark: 无截图,走手填亮点文本分析") raw = await clients.gpt_vision_analyze(prompt, images) result = parse_benchmark_json(raw) result["source"] = "vision" if images else "text" return result except Exception as e: logger.warning("analyze_benchmark failed, fallback: %s", e) result = fallback_benchmark(highlights) result["warning"] = f"标杆分析失败,已兜底:{e}" return result