""" app/services/ai_engine/fission_prompt.py — 第11环裂变 prompt(对齐上线版 split.js) 裂变=1爆款→一次LLM出N套完整笔记包。重写对齐产品包上线版 split.js: - FISSION_SYSTEM 完整笔记包字段(含 dimension/audience/scene/painPoint/keywords/imagePlan) - 参考度连续 30-85(reference_strategy_from_level),低≤40/高≥80/中 - 违禁词清洗 sanitize_image_plan_text - 品类兜底 build_fallback_notes / infer_category(LLM挂时不卡用户) 🔴 参考度入参保留 low/mid/high 三档(倩倩姐拍板),内部 _LEVEL_TO_INT 映射成数值。 🔴 三档真实业务措辞待北哥定义;本次接口按数值做对。 🔴 LLM 走 chat_complete(OpenAI兼容),FISSION_SYSTEM 作 messages[0].role=system 传。 """ from __future__ import annotations # 三档→数值映射(保留枚举入参,内部走连续值逻辑,对齐上线版 referenceStrategyFromLevel) _LEVEL_TO_INT = {"low": 35, "mid": 60, "high": 82} def reference_strategy_from_level(level: str | int) -> dict: """参考度策略(对齐上线版 referenceStrategyFromLevel)。 入参支持枚举 low/mid/high 或数值;钳到 30-85。 返回 {level, level_label, prompt_rule, summary}。 """ if isinstance(level, str): value = _LEVEL_TO_INT.get(level, 60) else: try: value = int(level) except (TypeError, ValueError): value = 60 value = max(30, min(85, value)) if value <= 40: return { "level": value, "level_label": "低参考", "prompt_rule": "只参考爆款的内容结构和图文角色,不沿用原文表达、标题句式和具体画面。", "summary": "参考爆款结构,不贴近原文表达。", } if value >= 80: return { "level": value, "level_label": "高参考", "prompt_rule": "强参考爆款的标题节奏、痛点切入、图文递进和情绪强度,但必须替换人群、场景、表达和图片,不得抄袭。", "summary": "强参考爆点、标题节奏和图文递进,但替换表达避免相似。", } return { "level": value, "level_label": "中参考", "prompt_rule": "参考爆款的结构、痛点表达方式和标题节奏,同时重写正文、标签和每张图片画面。", "summary": "参考结构、痛点和标题节奏,输出新的完整笔记包。", } def valid_level(level: str | None) -> str: """校验三档枚举,非法回落 mid(保留旧接口兼容)。""" return level if level in _LEVEL_TO_INT else "mid" # 默认裂变维度(对齐上线版 dimensions) DEFAULT_DIMENSIONS = ["换角度", "换痛点", "换人群"] FISSION_SYSTEM = """你是小红书完整图文笔记裂变专家。 你必须基于爆款参考生成"完整小红书笔记包",不是只生成文案,也不是只生成图片提示词。 完整笔记包必须包含:标题(可直接发布)、正文(180-260字种草口吻真实场景卖点转买点)、标签(5-8个)、点击钩子标题(第1张图大字)、imagePlan(每张图的图上文字+画面内容+排版)、dimension(裂变维度)、keywords、audience(适用人群)、scene(使用场景)、painPoint(切入痛点)。 裂变规则: - 每套必须不重复,标题/正文/标签/图文结构都要变,图片重新配套不可一套图反复发 - 保持种草安利+情绪共鸣风格 - 正文自然出现2-5个小红书符号/emoji(✅✨🌿💧📦🔍🧡🥹‼️),放在痛点/实测/选择理由/软性转化处,不堆砌不每句塞 - 标题可适度带符号,但不要所有标题同一种符号 - 图片=可上传的独立3:4图文海报,不是App截图/笔记详情页截图 - 图片禁止出现Like/评论/分享/底栏/头像/状态栏等社交App界面元素 - 对比页只做质地/场景/肤感说明对比,禁用前后、before/after、变白、瑕疵消失、治疗前后 - imagePlan只写短标题/短卖点/短画面要求,不塞长正文 - 禁用词:美白、祛斑、速效、医用、药妆、变白、before、after、使用前后 - 图文张数叙事:3张=点击→核心证明→软性转化;6张=点击→痛点→证明→质感→背书→软性转化;8张=点击→痛点→证明→质感→多场景→教程→背书→软性转化 - 最后一张是软性转化,不做淘宝式硬广;用囤货/省钱情报/搜索暗示/评论暗号等原生分享口吻 返回纯JSON数组,每个元素含:title/content/tags(数组)/coverTitle/dimension/audience/scene/painPoint/keywords(数组)/imagePlan(数组,每项{role,title,overlayText,text})。 硬性格式要求: - 只输出JSON,不要markdown代码块 - 字符串内部不用英文双引号,引用词用中文书名号或中文引号 - content是客户可直接发布的正文,不能写"配图建议/图片方向/imagePlan/内页规划"等内部提示 - imagePlan数量必须等于用户要求的图片数量""" import re # 违禁词清洗替换表(对齐上线版 sanitizeImagePlanText,有序应用) _SANITIZE_RULES = [ (re.compile(r"before\s*&\s*after", re.I), "质地与肤感说明"), (re.compile(r"before\s*/?\s*after", re.I), "质地与肤感说明"), (re.compile(r"\bbefore\b", re.I), "质地状态"), (re.compile(r"\bafter\b", re.I), "上脸肤感"), (re.compile(r"使用前后|用前用后|用前后|前后对比|使用前|使用后"), "质地/场景/肤感说明"), (re.compile(r"功效对比|效果对比|改善对比"), "质地/场景说明对比"), (re.compile(r"肤色变白|皮肤变白|变白|美白"), "自然光泽感"), (re.compile(r"瑕疵消失|斑点消失|痘印消失|消除瑕疵|祛斑"), "妆感更服帖"), (re.compile(r"治疗前后|治疗后|医美前后|治愈|修复受损"), "日常使用场景说明"), ] def sanitize_image_plan_text(value: str = "", max_length: int = 56) -> str: """清洗 imagePlan 文字里的违禁词(对齐上线版)。""" text = str(value or "") for pattern, repl in _SANITIZE_RULES: text = pattern.sub(repl, text) text = re.sub(r"\s+", " ", text).strip() return text[:max_length] def normalize_tags(tags=None, keywords=None) -> list[str]: """标签归一化:补#前缀、去重、截8个(对齐上线版 normalizeTags)。""" tags = tags or [] keywords = keywords or [] if not isinstance(tags, list): tags = str(tags).split() from_tags = [ t if str(t).strip().startswith("#") else f"#{str(t).strip()}" for t in tags if str(t).strip() ] from_kw = [ k if str(k).startswith("#") else f"#{k}" for k in list(keywords)[:5] if str(k).strip() ] seen, out = set(), [] for t in from_tags + from_kw: if t not in seen: seen.add(t) out.append(t) return out[:8] def build_fission_prompt( source_note: dict, product: dict, reference_level: str, note_count: int, image_count: int, dimensions: list[str] | None = None, ) -> str: """组装裂变 user prompt(对齐上线版 handleContentSplit 的 prompt 变量拼装)。 D3修复:给每套分配正交叙事主线(复用 TEXT_NARRATIVE_BY_STRATEGY A/B/C), 套数超过3时按顺序循环(套4=A/套5=B),让各套开篇/背书/收尾正交拉开。 """ from app.services.ai_engine.constants import TEXT_NARRATIVE_BY_STRATEGY src = source_note or {} prod = product or {} dims = dimensions or DEFAULT_DIMENSIONS strategy = reference_strategy_from_level(reference_level) title = src.get("title", "") content = src.get("content") or src.get("text", "") tags = src.get("tags", []) or [] name = prod.get("name", "") or "未提供" points = prod.get("selling_points", []) or [] audience = prod.get("target_audience", "") or "未提供" keywords = prod.get("keywords", []) or [] # D2修复:去掉"真实测评/好物分享"泛词兜底(评分prompt明确泛词占≥50%最高6分,自打自) # 改用产品真实信息提炼精准词;实在无keywords也不塞流量泛词 if keywords: kw_line = "、".join(keywords) else: # 从产品卖点/人群/名称里取实质性词,不拼平台泛词 sp_words = [str(s).strip() for s in (points[:2] if points else []) if str(s).strip()] kw_parts = [x for x in ([name] + sp_words + ([audience] if audience != "未提供" else [])) if x and x != "未提供"] kw_line = "、".join(kw_parts) if kw_parts else "" # D3修复:按套序循环分配 A/B/C 叙事主线 narrative_keys = list(TEXT_NARRATIVE_BY_STRATEGY.keys()) # ["A","B","C"] narrative_lines = [] for i in range(note_count): key = narrative_keys[i % len(narrative_keys)] narrative_lines.append(f"第{i+1}套叙事主线:{TEXT_NARRATIVE_BY_STRATEGY[key]}") narrative_block = "\n".join(narrative_lines) return f"""爆款参考: 标题:{title} 正文:{content} 标签:{' '.join(tags)} 产品:{name} 产品卖点:{'、'.join(points) or '未提供'} 目标人群:{audience} 关键词:{kw_line} 裂变维度:{'、'.join(dims)} 爆款参考度:{strategy['level_label']}。{strategy['prompt_rule']} 生成数量:{note_count}套 每套图片数量:{image_count}张 【各套正交叙事主线——必须严格执行,这是本次裂变不同质的关键】 {narrative_block} 请生成{note_count}套完整小红书图文笔记包。每套都必须含标题、正文、标签、点击钩子标题、{image_count}张图的imagePlan。 每套必须严格按其指定叙事主线展开开篇/背书/收尾,不同套之间叙事结构必须正交、不重复。 imagePlan必须按叙事链路逐张递进。3张版第2张必须是按当前品类变化的核心证明页,不能和第1张重复构图,不得让第2张重复第1张标题。 正文必须像真实小红书种草笔记一样自然带2-5个emoji;不要把图片规划/配图建议/内部审核建议写进正文。""" def notes_array_from_parsed(parsed) -> list[dict]: """从LLM解析结果里拎出笔记数组(对齐上线版 notesArrayFromParsed)。""" if isinstance(parsed, list): return parsed if not isinstance(parsed, dict): return [] for key in ("notes", "variants", "data", "items", "result", "results"): if isinstance(parsed.get(key), list): return parsed[key] return [parsed] if (parsed.get("title") or parsed.get("content")) else []