Files
beige/backend/app/services/ai_engine/fission_prompt.py
yangqianqian d85dcd401b 第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>
2026-06-18 11:17:37 +08:00

184 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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-85reference_strategy_from_level低≤40/高≥80/中
- 违禁词清洗 sanitize_image_plan_text
- 品类兜底 build_fallback_notes / infer_categoryLLM挂时不卡用户
🔴 参考度入参保留 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 变量拼装)。"""
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 []