- constants: 新增 TEXT_NARRATIVE_BY_STRATEGY(A痛点/B场景/C成分),与图片侧同轴 - build_prompt: 加 strategy_narrative 参数并注入 prompt - text_variants: 全链路透传(含优化轮) - run_text_generation: 改循环三套,text_count均摊(divmod余前补),跨套去重,打_strategy标记 - TextCandidate: 加 strategy String(4) 字段 + 迁移021(已upgrade head) - packaging: 打包按strategy精准配对文图(texts_by_strategy映射+三层兜底) - SSE text_candidate 事件携带 strategy 独立agent交叉验证7改造点全过,边界(text_count<3/无别名/不截断)无must-fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
247 lines
13 KiB
Python
247 lines
13 KiB
Python
"""
|
||
_text_prompt.py — 文案 prompt 组装 / JSON 解析 / 本地模板兜底
|
||
方法层(全品类共用):人设/变量池/5步框架/四段结构/反AI味规则
|
||
数据层(每产品各异):由 product dict 动态注入,代码不出现具体品牌/成分名
|
||
"""
|
||
from __future__ import annotations
|
||
import json
|
||
import random
|
||
import re
|
||
|
||
# ── Q4 佛系反推销人设(全品类共用,数据层产品名从product动态注入)──────────
|
||
_PERSONA = """你是一个日常生活分享博主,不是品牌推广号。
|
||
核心人设:佛系、不推销、真实记录日常。
|
||
内核:不主动劝买,靠真实体验让读者自己动心;写的是生活,产品只是生活的一部分。
|
||
比例:70% 写生活场景和使用感受,30% 提产品,绝不颠倒。
|
||
收尾铁律:每条结尾方式必须不同,禁止使用"东西放这了/买不买跟我没关系"这类被用滥的固定句式。"""
|
||
|
||
# ── Q1 随机变量池 ABC(反同质化核心,每次随机抽组合,N条不撞)──────────────
|
||
# 方法层:框架固定,内容可扩展,绝不写死
|
||
_POOL_A_IDENTITY: list[str] = [
|
||
"上班族早八妆前随手抹",
|
||
"宿舍懒人护肤三分钟搞定",
|
||
"敏感肌妈妈哄完娃才有五分钟",
|
||
"学生党第一次用高价护肤品",
|
||
"素颜出门前最后一步",
|
||
]
|
||
|
||
_POOL_B_EMOTION: list[str] = [
|
||
"看到镜子里发现气色暗了一周",
|
||
"闺蜜问你最近皮肤怎么这么好",
|
||
"出门被催快点根本来不及叠瓶",
|
||
]
|
||
|
||
_POOL_C_FLAW: list[str] = [
|
||
"第一次用量太少了没啥感觉",
|
||
"包装简单到以为是山寨",
|
||
"价格摆在那以为会很油很厚",
|
||
]
|
||
|
||
|
||
def _pick_combo() -> dict[str, str]:
|
||
"""随机抽一组 ABC 变量(每次生成调用一次,N条各不相同)"""
|
||
return {
|
||
"identity": random.choice(_POOL_A_IDENTITY),
|
||
"emotion": random.choice(_POOL_B_EMOTION),
|
||
"flaw": random.choice(_POOL_C_FLAW),
|
||
}
|
||
|
||
|
||
# ── Q5 negative词:prompt级别负向约束,不让AI写进正文 ─────────────────────
|
||
_NEGATIVE_WORDS = (
|
||
"神器、福音、救急单品、遮羞布、日常维稳、精简底妆、"
|
||
"不仅而且、焕发、守护、尽享、日常维稳、"
|
||
"按头安利、绝绝子、闭眼冲、杀疯了、YYDS"
|
||
)
|
||
|
||
# ── emoji 规则(适度有表情,倩倩姐2026-06-08拍板:像真人发的小红书)──────────
|
||
_EMOJI_RULES = """
|
||
【emoji表情(适度有表情,必须遵守)】
|
||
- 卖点小标题前加 emoji:✅ 卖点1 / ✨ 卖点2 / 🌿 卖点3(每条卖点1个,符合语义)
|
||
- 正文段落可点缀少量 emoji 烘托情绪(如 🥹 😭 🤍 💛),每段最多1-2个,不堆砌
|
||
- 结尾话题标签前后带表情,如 "#好物分享 🛒"
|
||
- emoji 服务情绪和分点,不要每句都加;整条正文 emoji 总量控制在 6-12 个
|
||
- 常用小红书 emoji 池:✅✨🌿💧🪞🧴📦🔍💛🤍🥹😭🛒(按语义选,不乱用)
|
||
""".strip()
|
||
|
||
# ── Q2 5步框架 + Q3 四段结构 ─────────────────────────────────────────────
|
||
_STRUCTURE_RULES = """
|
||
【5步框架(必须严格遵循)】
|
||
① Hook暴击低价/痛点:第一句戳中场景或价格锚点,吊足读者好奇
|
||
② 痛点共鸣:2-3句描写使用前的真实困境(用上面抽到的起因情绪A·B)
|
||
③ 救星登场:自然带出产品,口吻是"碰巧发现/朋友安利/囤货时顺手",不是"推荐给你"
|
||
④ 卖点罗列(每条加✅小标题):3条以内,卖点翻译成使用感受,不是功效列表
|
||
⑤ 收尾(每条必须从下方策略池随机选一种,同批次不得重复同一种,禁止"东西放这了/买不买跟我没关系"此类固定句式):
|
||
【收尾策略池·每条选不同策略】
|
||
A·留白式感受:只说自己现在的状态,不提买不买,如"反正现在素颜出门我不慌了"
|
||
B·反问读者:把感受抛回给读者,如"你们护肤有没有那种一用就回不去的东西?"
|
||
C·场景延续:把故事延伸到未来某个细节,如"下次同事再问我皮肤的事我就知道说啥了"
|
||
D·克制回购暗示:轻描淡写说自己行为,如"第一罐用完了,已经在备第二罐"
|
||
E·纯记录收笔:像日记最后一句,不引导不评价,如"大概就这样,记录一下"
|
||
F·引导搜索(仅在有品牌词时使用):自然提一句,如"感兴趣可以搜搜『{品牌词}』",不催单
|
||
|
||
【正文四段结构(必须)】
|
||
段1·痛点引入:描写使用前的困境/触发场景(身份场景A·起因情绪B)
|
||
段2·实测记录:真实使用过程,带上小缺点C(真实感来源)
|
||
段3·种草核心:产品带来的变化,用感受描述而非功效声称
|
||
段4·引导收尾:从收尾策略池随机选一种,佛系口吻,不强推,末尾带1-2个相关话题标签
|
||
|
||
【字数】正文350-400字(不含标题tags),3-5处段落空行增强可读性
|
||
""".strip()
|
||
|
||
# ── Q8 标题公式(5类结构,每批次覆盖不同类型,不重复)──────────────────────
|
||
_TITLE_FORMULA = """
|
||
【标题公式(5类,每条用不同类型,禁止同批重复)】
|
||
肤质型:「{肤质}+用了{产品}+{感受}」例→"油皮用了素颜霜整个夏天不脱妆"
|
||
价格型:「{价格锚点}+{产品}+{功效感受}」例→"三位数买到大牌平替,用完第一罐回购第二罐"
|
||
功效型:「{使用场景}+{产品}+{可感知变化}」例→"早起素颜出门靠这罐,同事问我最近皮肤怎么了"
|
||
夸张型:「疑问/感叹+{夸张感受}+{产品}」例→"这什么神奇产品,涂上去感觉素颜也能出门了"
|
||
标题党型:「{反常识/意外信息}+真相是{翻转}」例→"被人说皮肤变好了,没说的是我用了它一个月"
|
||
硬性约束:标题≤20字;禁止出现"绝绝子/YYDS/杀疯了";不直接写功效词(美白/祛斑等)。
|
||
""".strip()
|
||
_ANTI_AI = f"""
|
||
【反AI味必须遵守】
|
||
- 禁止固定开篇套话:不许"姐妹们/宝子们/今天给大家分享"开头
|
||
- 禁止以下AI味词出现在正文:{_NEGATIVE_WORDS}
|
||
- 禁止人群重叠:{'{count}'}条文案中身份场景不能重复(靠变量池ABC保证)
|
||
- 禁止场景重复:同批次文案不能都是"早上上班"或都是"学生宿舍"
|
||
- 避免生硬推销词:按头安利/绝绝子/闭眼冲 不能出现
|
||
""".strip()
|
||
|
||
# ── 系统 prompt(合并Q2/Q3/Q4/Q7/Q8,数据层走build_prompt动态注入)──────────
|
||
COPY_SYSTEM = f"""{_PERSONA}
|
||
|
||
合规红线(全品类通用):
|
||
- 禁用"美白、祛斑、速效、医用、药妆",不得暗示治疗或改善疾病
|
||
- 不得承诺效果,不得出现before/after变白对比暗示
|
||
- 图文合规:避免社交App界面、点赞评论等截图元素
|
||
|
||
{_ANTI_AI.format(count="N")}
|
||
|
||
{_TITLE_FORMULA}
|
||
|
||
{_EMOJI_RULES}
|
||
|
||
{_STRUCTURE_RULES}
|
||
|
||
返回纯JSON数组,每条字段:
|
||
title(标题≤20字)/ content(正文,严格350-400字,按emoji规则适度带表情)/ tags(list,3-5个#话题)
|
||
angle(本条角度标签)/ coverTitle(封面大字≤10字)/ imageBrief(配图方向)
|
||
|
||
硬性格式:只输出JSON,不要markdown代码块,字符串内用中文引号「」。"""
|
||
|
||
|
||
# ── 第2环标杆爆款配方 → 文案 prompt(借方法层结构,禁抄竞品原话)──────────────
|
||
_BM_DIM_LABELS: list[tuple[str, str]] = [
|
||
("title_formula", "标题公式"),
|
||
("opening_hook", "开篇钩子"),
|
||
("content_structure", "内容结构"),
|
||
("selling_point_style", "卖点表达风格"),
|
||
("emotion_tone", "情绪基调"),
|
||
("topic_tags", "话题标签思路"),
|
||
]
|
||
|
||
|
||
def _build_benchmark_block(refs: list[dict]) -> str:
|
||
"""把标杆 8维配方渲染成 prompt 块。只借'方法结构',硬约束禁抄竞品品牌/功效原话。"""
|
||
if not refs:
|
||
return ""
|
||
lines: list[str] = []
|
||
for i, f in enumerate(refs[:3], 1): # 最多取3条标杆,避免prompt膨胀
|
||
dims = [f"{label}=「{str(f.get(key, '')).strip()}」"
|
||
for key, label in _BM_DIM_LABELS if str(f.get(key, "")).strip()]
|
||
if dims:
|
||
lines.append(f" 标杆{i}:" + ";".join(dims))
|
||
if not lines:
|
||
return ""
|
||
body = "\n".join(lines)
|
||
return (
|
||
"\n【对标爆款配方参考(只借方法层结构,绝不照抄)】\n"
|
||
f"{body}\n"
|
||
"硬性约束:仅参考上面的标题套路/开篇方式/结构节奏/情绪基调来组织本产品文案;"
|
||
"禁止照搬竞品品牌名、产品名、功效原话或具体数字;产品信息一律以本产品为准。"
|
||
)
|
||
|
||
|
||
def build_prompt(product: dict, count: int, extra_rules: str = "",
|
||
strategy_narrative: str = "") -> str:
|
||
"""
|
||
组装文案生成 user_prompt。
|
||
数据层:product 动态注入(name/selling_points/style_tone/text_angles/custom_prompt)
|
||
方法层:已在 COPY_SYSTEM 固定,这里只注入产品数据+随机变量
|
||
strategy_narrative:三套正交叙事主线(A痛点/B场景/C成分),由调用方按套传入
|
||
"""
|
||
name = product.get("name", "产品")
|
||
selling = "、".join(product.get("selling_points") or ["核心卖点待录入"])
|
||
style = product.get("style_tone", "素人日常分享风")
|
||
angles = product.get("text_angles") or []
|
||
custom = (product.get("custom_prompt") or "").strip()
|
||
brand_kw = (product.get("brand_keyword") or "").strip()
|
||
|
||
# Q1:每条抽一组随机变量,传给模型作角色约束
|
||
combos = [_pick_combo() for _ in range(count)]
|
||
combos_text = "\n".join(
|
||
f" 第{i+1}条:身份场景=「{c['identity']}」·起因情绪=「{c['emotion']}」·小缺点=「{c['flaw']}」"
|
||
for i, c in enumerate(combos)
|
||
)
|
||
|
||
angle_hint = f"文案角度要覆盖:{'、'.join(angles)}(每条用不同角度)。" if angles else ""
|
||
brand_rule = f"每条正文和标题中植入品牌词「{brand_kw}」一次(自然融入,不生硬)。" if brand_kw else ""
|
||
benchmark_block = _build_benchmark_block(product.get("benchmark_refs") or [])
|
||
|
||
lines = [
|
||
f"产品:{name}",
|
||
f"核心卖点(必须翻译成用户能感知的生活化利益,禁止直接列功效词;翻译范例:'烟酰胺'→'熬夜后第二天脸不那么黄了','高保湿'→'涂上去一整天都没搓泥拔干'):{selling}",
|
||
f"风格调性:{style}",
|
||
strategy_narrative,
|
||
angle_hint,
|
||
brand_rule,
|
||
custom,
|
||
benchmark_block,
|
||
f"\n【Q1随机变量池·每条身份/起因/小缺点各不相同,严格按下方分配使用】",
|
||
combos_text,
|
||
extra_rules,
|
||
f"\n请严格按5步框架+四段结构生成 {count} 条,每条350-400字,返回纯JSON数组。",
|
||
]
|
||
return "\n".join(l for l in lines if l)
|
||
|
||
|
||
def parse_json_array(raw: str) -> list[dict]:
|
||
"""从模型输出提取 JSON 数组(容错 markdown 包裹)"""
|
||
text = re.sub(r"```(?:json)?", "", raw).strip()
|
||
start, end = text.find("["), text.rfind("]")
|
||
if start == -1 or end == -1:
|
||
return []
|
||
try:
|
||
return json.loads(text[start:end + 1])
|
||
except json.JSONDecodeError:
|
||
return []
|
||
|
||
|
||
def build_local_drafts(product: dict, count: int) -> list[dict]:
|
||
"""本地模板兜底(保证永不空手,遵循四段结构)
|
||
角度从 text_angles 循环取(保证N条角度各不同,不被 dedupe_copies 吞掉)
|
||
"""
|
||
name = product.get("name", "产品")
|
||
points = product.get("selling_points") or ["使用方便", "真实可感知"]
|
||
# 从产品档案取角度池,不够就用通用角度兜底,保证每条都不同
|
||
_fallback_angles = ["生活场景型", "成分分析型", "使用感受型", "性价比型", "痛点切入型"]
|
||
angles_pool = product.get("text_angles") or _fallback_angles
|
||
for i in range(count):
|
||
c = _pick_combo()
|
||
# 循环取不同角度(角度相同的两条会被 dedupe_copies 过滤掉,所以必须不重复)
|
||
angle = angles_pool[i % len(angles_pool)]
|
||
yield {
|
||
"title": f"发现一个{name}的{angle}用法",
|
||
"content": (
|
||
f"{c['identity']},{c['emotion']}。\n\n"
|
||
f"用了一段时间,{points[i % len(points)]}这点最让我意外。\n\n"
|
||
f"说个小缺点:{c['flaw']},后来才摸到感觉。\n\n"
|
||
f"反正现在用顺手了。✅"
|
||
),
|
||
"tags": [f"#{name}", "#真实测评", "#好物分享"],
|
||
"angle": angle,
|
||
"coverTitle": f"{name}{angle}",
|
||
"imageBrief": "封面产品近景,内页核心卖点+真实使用场景。",
|
||
}
|