第11环裂变重写:对齐上线版 split.js 一次LLM出N套完整笔记包

架构从"扇出N个GenerationTask各跑完整管道"改为"一次LLM调用直接出N套
完整笔记包(N=1~5)",落 FissionNote 表 + 独立展示页。

后端:
- 018迁移:fission_notes 表(文案JSON+score+passed+imagePlan+images+status)
- fission_prompt:FISSION_SYSTEM+三档参考度(low/mid/high)+normalize_tags+品类兜底
- fission_pipeline:一次LLM出N套→各评分(@80合格线)→排序→落库,不达标标
  needs_optimization 非丢弃;apiports 503 回落 codeproxy gpt-5.5 强档兜底
- fission_images:每套串行调现有生图接口(零改动image_gen/storyboard)
- tasks.py:run_fission_pipeline Celery task,删旧扇出注入
- api/v1/fission:进度聚合FissionNote + GET /fission/{id}/notes(剥内部字段)

前端:FissionProgress对齐状态机 + N套独立展示页 + FissionNoteCard

测试:test_fission_engine(19)+test_fission_pipeline(5) 全过;104 全量回归绿

实测task5(fanout=2,mid)端到端跑通:一次出2套→seq0=85过/seq1=79标优化→
生图codeproxy/edits→1024×1536去AI化→task completed→notes端点返完整数据

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-18 11:17:37 +08:00
parent 7f419f4c8b
commit d85dcd401b
18 changed files with 1772 additions and 106 deletions

View File

@@ -1,43 +1,183 @@
"""
app/services/ai_engine/fission_prompt.py — 第11环裂变 三档参考度 prompt
app/services/ai_engine/fission_prompt.py — 第11环裂变 prompt(对齐上线版 split.js
裂变=1爆款→N套完整笔记包。参考度档位控制"新套子贴原爆款多紧"
high=贴原爆款结构+卖点(最像) / mid=保卖点换叙事角度 / low=只借选题方向(最自由)
裂变=1爆款→一次LLM出N套完整笔记包。重写对齐产品包上线版 split.js
- FISSION_SYSTEM 完整笔记包字段(含 dimension/audience/scene/painPoint/keywords/imagePlan
- 参考度连续 30-85reference_strategy_from_level低≤40/高≥80/中
- 违禁词清洗 sanitize_image_plan_text
- 品类兜底 build_fallback_notes / infer_categoryLLM挂时不卡用户
🔴 占位规则(倩倩姐2026-06-16:先移植打通再请北哥定档位)
档位真实措辞待北哥定义后替换 _LEVEL_RULES,引擎链路不动
🔴 参考度入参保留 low/mid/high 三档(倩倩姐拍板),内部 _LEVEL_TO_INT 映射成数值
🔴 三档真实业务措辞待北哥定义;本次接口按数值做对
🔴 LLM 走 chat_complete(OpenAI兼容)FISSION_SYSTEM 作 messages[0].role=system 传。
"""
from __future__ import annotations
# 三档占位规则:注入文案引擎 flywheel_context约束"参考原爆款多少"
_LEVEL_RULES = {
"high": (
"【裂变参考度=高】请紧贴下面这篇原爆款的正文结构、开头钩子、卖点排布与情绪语气,"
"近乎仿写——只替换表达措辞做到不查重,骨架与卖点角度都保留。"
),
"mid": (
"【裂变参考度=中】请保留下面这篇原爆款的核心卖点,但换一个全新的叙事角度/切入场景重写,"
"结构可调整,让读者看不出是同一套路。"
),
"low": (
"【裂变参考度=低】请只借鉴下面这篇原爆款的选题方向与目标人群,"
"文案结构、卖点呈现、叙事全部自由发挥,做出明显差异化的新笔记。"
),
}
# 三档→数值映射(保留枚举入参,内部走连续值逻辑,对齐上线版 referenceStrategyFromLevel
_LEVEL_TO_INT = {"low": 35, "mid": 60, "high": 82}
def build_fission_context(source_note: dict, reference_level: str) -> str:
def reference_strategy_from_level(level: str | int) -> dict:
"""参考度策略(对齐上线版 referenceStrategyFromLevel
入参支持枚举 low/mid/high 或数值;钳到 30-85。
返回 {level, level_label, prompt_rule, summary}。
"""
返回注入 text_variants(flywheel_context=) 的档位规则字符串。
source_note: {title, content, ...} 原爆款笔记内容。
reference_level: high/mid/low非法值回落 mid。
"""
rule = _LEVEL_RULES.get(reference_level, _LEVEL_RULES["mid"])
title = (source_note or {}).get("title", "")
content = (source_note or {}).get("content", "")
src = f"原爆款标题:{title}\n原爆款正文:{content}".strip()
return f"{rule}\n\n--- 原爆款参考 ---\n{src}"
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_RULES else "mid"
"""校验三档枚举,非法回落 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 变量拼装)。"""
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 []
kw_line = "".join(keywords) if keywords else "".join(
[x for x in [name, audience, "真实测评", "好物分享"] if x and x != "未提供"]
)
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}
请生成{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 []