market-assistant/backend/pipeline/llm/generate_marketing_detail.py
hub-gif 614d631261 feat(pipeline): 竞品简报与策略管线、报告导出及 JD 前端联动
补充策略决策键、fixture 与相关单测;分析/营销包本地存储与任务状态;竞品简报 schema 与 LLM 章节生成、促销与导出调整;并纳入 Cursor 工程规则 user-dev-style。

Made-with: Cursor
2026-04-27 15:48:09 +08:00

213 lines
16 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.

"""策略稿 → 核心信息卡 → 营销内容多触点文案(两步 LLMJSON 输出)。"""
from __future__ import annotations
import json
from typing import Any
from .llm_client import call_llm
_MAX_STRATEGY_CHARS = 28_000
CORE_CARD_SYSTEM = """你是电商营销内容顾问。根据用户提供的「策略稿全文」与结构化决策、业务备注,输出**仅一段 UTF-8 JSON 对象**(不要 Markdown 代码围栏,不要前后说明文字)。
**硬性**
- 事实、数字、功效、检测结论、销量、评价原文:**仅可**来自输入;**禁止**编造未出现的品牌名、数据、「用户说」引语。
- 食品/健康相关:**禁止**治疗承诺与夸大疗效;无依据写「输入未体现」或「待法务确认」。
- 句子短、可落地;兼顾**购买者决策**与**列表/商详/主图等多触点**上架可用性。
- **读者第一眼须知道在卖什么**:禁止通篇只有「价值感」「信任」「体验」而**不出现可识别的品类/形态**(如饼干、燕麦、奶粉、饮料等)。若输入未给出具体 SKU 名,仍须写清**类目 + 形态/规格层级**(如「低 GI 方向早餐饼干(待业务定款)」),不得用「优质好物」「健康之选」等**无品类**的句子糊弄本条。
- **对照式理由(不编造)**:当策略或备注能概括「普通/常规同类」的典型痛点(如升糖快、甜腻、纤维低、易饿)时,**why_this_product** 与 **differentiation_vs_alternatives** 须用**对照**写清「为何选本品」;**禁止**捏造未出现的品牌、检测值、具体「高/低百分之几」等。无对标素材时写本品独特点,并在 **open_points_for_business** 提示可补充的对照数据或检测依据(若无则空串)。
**JSON 键(须全部出现,值为字符串;无内容用空串)**
- what_we_sell**卖的是什么**(必填,建议 2580 字)。写清**品类 + 主推形态/规格或适用场景**,让读者**不读策略稿**也能回答「你们在卖哪种货」。**仅可**综合策略稿、`strategy_decisions`(尤其 **pillar_product**、battlefield_one_line、audience_segment、marketing_strategy、`business_notes` 与 `keyword` 监测语境中已出现的信息;若 `pillar_product` 非空须与之**不矛盾**。无具体商品名时须明确写「待业务补充主推 SKU/品名」,并保留类目词(可与关键词监测范围对读)。
- one_liner_value一句话价值主张买家能得到什么
- buyer_job_to_be_done购买者的任务或情境一句
- key_pain_or_desire核心痛点或欲望与策略一致
- why_this_product为何要选这一款**优先** 12 句写相对**常规/普通同类**的核心理由可用泛称如「普通甜面包」「常见饼干」可从蛋白、膳食纤维、饱腹感、GI 或糖负担、口感、包装形态、配料表等**择输入已支持**的维度;无对照素材则写本品独特点)
- proof_or_trust_angle信任或证明角度无依据写「输入未体现」
- differentiation_vs_alternatives与替代方案相比的差异**须含**与常规同类对照的一句话结论营养数字、GI、每百克含量等**仅可**复述输入已有内容)
- price_value_framing价位与价值感如何表述与策略价位可对读无则「待业务确认」
- compliance_taboos表述禁区摘要来自业务备注或策略风险
- open_points_for_business待业务补充无则空串
"""
DETAIL_PACK_SYSTEM = """你是京东场景营销内容写手。输入为已定稿的「核心信息卡」JSON 与关键词。请输出**仅一段 UTF-8 JSON 对象**(不要 Markdown 代码围栏)。
**硬性**
- **仅可**依据核心信息卡展开;**禁止**新增数字、功效、认证、评价引语、竞品具体名(除非信息卡里已有)。
- 购买者视角,短句;禁止输出 JSON 键名英文给最终读者(值全部为中文**多触点上架**可用文案)。
- 不要泄露「核心信息卡」「策略稿」等内部词。
- **更丰富≠编造**:可增加条数与段落,但**每一条**须能从信息卡对应字段找到方向;无依据处写「输入未体现」「待业务核对」,**禁止**为凑字数新增数字、销量、认证、评价引语、具体竞品名。
- **对照式表达(写厚但不编造)**:学习优质商详「先对比再购买」。**detail_headline** 在首句点明品类后,**至少 1 句**用「相对普通/常规同类(泛称,禁止编造品牌)」讲清差异或价值;无依据时用中性句或「具体对比数值待包装/检测与业务核对」。**selling_bullets** 中 **至少 3 条**须为**可感知的对照卖点**,从蛋白、膳食纤维、饱腹感、口感质地、配料/清洁标签、包装控量或便携、GI/糖负担等角度择信息卡**已支持**的项;信息卡未提的维度**不硬写**。**detail_mid_story_paragraphs** 中 **至少 1 段**用「为何不满足于普通同类」叙事,仍须紧扣信息卡,禁止新数字与编造用户故事。
- **每条 listing_titles、listing_subtitle、detail_headline、selling_bullets 的前两条**均须让读者能识别**在卖什么品类/什么货**(须与信息卡 **what_we_sell** 一致,可缩写但**禁止**偷换品类或只剩空洞形容词)。若信息卡 `what_we_sell` 已写品类,文案中**至少一处**直接出现该类目词或同义可识别表述。
- **文生图/文生视频提示词**:须为**可直接复制**到常见文生图、文生视频模型的**中文**描述;**仅可**依据信息卡已有事实与品类,**禁止**在提示词里写「策略稿」「信息卡」「JSON」等元话语**禁止**要求生成未授权的具体品牌 Logo、真实包装上的可辨认商标、带疗效承诺的贴片字。
- **文生图须「有货、有卖点画面」**(硬性):
- 从信息卡 **what_we_sell**、**one_liner_value**、**key_pain_or_desire**、**why_this_product**、**differentiation_vs_alternatives**、**price_value_framing** 中提炼 **13 条可画出来的卖点**,写入 ``text_to_image_prompt_main``**场景图** ``text_to_image_prompt_scene`` 非空时须保留**至少 1 处**同款质地或品类辨识(非空场景图时)。
- **禁止**整段只有「白底」「居中」「电商主图」「健康食品」等空壳,而**不出现具体货态**(形态、切片、包装类型、手持/摆放方式至少择一)。
- **口感/质地类**(如松软、酥脆、绵密、有嚼劲):**必须**写成**可见结构**,不能只写一次形容词了事。例:**松软**→「吐司切片横截面气孔细腻、边缘微翘显蓬松」「轻按后缓慢回弹」「手撕开可见柔软内里」;**酥脆**→「饼干断面层次清晰、碎屑自然」。信息卡未提质地则**不写**,勿编造。
- **配料/品类视觉**(如全麦):可写「麸皮颗粒隐约可见」「浅褐全麦外皮」等,**禁止**疗效字幕、血糖仪、前后对比治病画面。
**JSON 键(须全部出现)**
- listing_titles字符串数组**69** 条商品短标题备选(每条约 30 字内;**每条须含可识别品类或品名线索**,禁止多条全是空洞套话;其中 **23 条**可在有依据时含「相对更…/更少…/不腻」等**对照**表述;其余侧重场景/质地/配料/人群)
- listing_subtitle一条列表副文案约 **60100** 字内,信息不足则取下限;**鼓励**含一句与常规同类对照的价值,无依据则省略)
- detail_headline商品详情页首屏下 lead**24 句****首句须点明卖的是什么货****至少 1 句**为相对常规同类的对照或价值;总长约 **80200** 字)
- selling_bullets字符串数组**812** 条卖点(每条约 **40 字内****至少 3 条**为「本品 vs 常规同类」式差异;整体须覆盖:品类形态、口感/质地(若信息卡有)、蛋白/纤维/饱腹/GI 或糖负担(**仅信息卡有则写**)、配料/健康表述(合规)、包装/规格(若信息卡有)、场景、信任点、与常规品差异等**不同角度****禁止** 12 条重复同一句话换说法)
- spec_sidebar_lines字符串数组**05** 条参数区旁短句(可空数组)
- faq对象数组每项含 question、answer 字符串,**58** 组;答句不得超出信息卡承诺;其中 **12** 组宜为「和普通/常规××有什么不同」类(××用泛称);可含「怎么保存」「适合谁」
- detail_mid_story_paragraphs字符串数组**24 段**详情页**首屏之后**的中段叙事;每段 **70150** 字;**仅**展开信息卡已有卖点与 `what_we_sell`,可分段讲「适合谁—怎么吃—为何值得」;**禁止**新数字、新功效、编造用户故事
- usage_and_pairing_tips字符串数组**25** 条食用场景、保存提示、搭配建议(如早餐配牛奶);信息卡未写保存条件则写「输入未体现具体保质期与保存要求,上架前请核对包装」类中性句,**禁止**编造保质期天数
- short_graphic_post_variants字符串数组**35** 条短图文/种草贴变体;每条 **45110** 字;须**首句或次句**点明品类;适合复制到站内动态;**禁止**销量名次、虚假好评引语
- live_script_bullets字符串数组**47** 条直播或短视频**可照读要点**(每条约 **1540** 字);按顺序像口播提纲;**禁止**医疗承诺与未证实数据;可与 `live_or_short_hook` 呼应但勿逐句重复
- traceability_note**依据与边界**必填24 句)。用业务可读中文说明:本包与信息卡中**哪些承诺方向一致**、**哪些表述须业务或法务核对**、**输入未体现的不得对外宣称****禁止**新数字、新功效、新认证。
- main_image_three_points字符串数组**恰好 3 条**,主图/首图用超短句(每条建议 614 字);须与 **what_we_sell** 品类一致,可来自卖点压缩,禁止空泛口号
- live_or_short_hook一条直播或短视频开场钩句≤40 字);同一事实约束
- customer_service_opening一条客服首句/欢迎语建议≤50 字);同一事实约束
- text_to_image_prompt_main字符串**主图/首图**文生图提示词(建议 **100260** 字)。**必须**依次包含:① **具体货态**(与 **what_we_sell** 一致的品类+形态,如全麦吐司切片摞放、独立小包饼干);② **至少一条质地/卖点的视觉化描写**(与信息卡一致,参见上文「松软→截面/按压/手撕」等);③ **构图与背景**(如白底居中、轻微投影);④ **光影**(柔和棚拍、写实);⑤ **规避**(无 Logo、无疗效字、无竞品名。**英文模型**可关键风格词括注英文。
- text_to_image_prompt_scene字符串**场景/生活方式**备选图(建议 **80200** 字):早餐桌、手持、厨房台面等;**须含**与主图**同一品类**的清晰货态,并**至少一处**质地或食用情境(如蒸汽、刀切截面、蘸牛奶)。与主图完全重复则宁可缩短但保留情境差分。无合适场景时 ``""``。
- text_to_video_prompt字符串文生视频提示词建议 **100260** 字),竖屏 9:16、**515 秒**。**须**含 **1 个能体现质地或卖点的镜头**(如慢镜撕开吐司见柔软内里、刀切截面特写、轻捏回弹),与信息卡卖点一致;另写开场与转场(推近/平移)。**禁止**疗效字幕、未授权标识;可「无对白」或「一句中性口播」。
"""
# 第二步 JSON 完整键表;模型漏键或旧落盘缺字段时由 ``normalize_detail_page_pack`` 补齐。
_DETAIL_PAGE_PACK_DEFAULTS: dict[str, Any] = {
"listing_titles": [],
"listing_subtitle": "",
"detail_headline": "",
"selling_bullets": [],
"spec_sidebar_lines": [],
"faq": [],
"traceability_note": "",
"main_image_three_points": [],
"live_or_short_hook": "",
"customer_service_opening": "",
"text_to_image_prompt_main": "",
"text_to_image_prompt_scene": "",
"text_to_video_prompt": "",
"detail_mid_story_paragraphs": [],
"usage_and_pairing_tips": [],
"short_graphic_post_variants": [],
"live_script_bullets": [],
}
_DETAIL_PAGE_PACK_LIST_KEYS: frozenset[str] = frozenset(
{
"listing_titles",
"selling_bullets",
"spec_sidebar_lines",
"main_image_three_points",
"faq",
"detail_mid_story_paragraphs",
"usage_and_pairing_tips",
"short_graphic_post_variants",
"live_script_bullets",
}
)
def normalize_detail_page_pack(data: dict[str, Any]) -> dict[str, Any]:
"""保证 ``detail_page_pack`` 含全部约定键,避免模型漏输出或旧 JSON 缺字段。"""
out: dict[str, Any] = dict(data)
for key in _DETAIL_PAGE_PACK_DEFAULTS:
v = out.get(key)
if key in _DETAIL_PAGE_PACK_LIST_KEYS:
if isinstance(v, list):
continue
out[key] = []
continue
if v is None:
out[key] = ""
elif not isinstance(v, str):
out[key] = str(v)
return out
def _truncate_strategy(md: str) -> tuple[str, bool]:
t = (md or "").strip()
if len(t) <= _MAX_STRATEGY_CHARS:
return t, False
return t[: _MAX_STRATEGY_CHARS].rstrip() + "\n\n…(策略正文已截断,以下同)\n", True
def _parse_llm_json(raw: str) -> dict[str, Any]:
s = (raw or "").strip()
if not s:
raise ValueError("大模型返回为空")
try:
out = json.loads(s)
except json.JSONDecodeError:
i = s.find("{")
j = s.rfind("}")
if i >= 0 and j > i:
out = json.loads(s[i : j + 1])
else:
raise ValueError("大模型返回不是合法 JSON") from None
if not isinstance(out, dict):
raise ValueError("大模型 JSON 须为对象")
return out
def generate_core_info_card(
*,
keyword: str,
strategy_markdown: str,
strategy_decisions: dict[str, Any] | None,
business_notes: str,
) -> dict[str, Any]:
md, truncated = _truncate_strategy(strategy_markdown)
payload = {
"keyword": keyword,
"strategy_markdown": md,
"strategy_markdown_truncated": truncated,
"strategy_decisions": strategy_decisions or {},
"business_notes": (business_notes or "").strip(),
}
user = (
"请根据以下 JSON 输出核心信息卡(仅 JSON 对象):\n"
+ json.dumps(payload, ensure_ascii=False)
)
raw = call_llm(CORE_CARD_SYSTEM, user)
return _parse_llm_json(raw)
def generate_detail_page_pack(
*,
keyword: str,
core_info_card: dict[str, Any],
) -> dict[str, Any]:
payload = {
"keyword": keyword,
"core_info_card": core_info_card,
}
user = (
"请根据以下 JSON 输出营销内容多触点文案(**仅**一段 JSON 对象)。\n"
"**必填键名(缺一不可,勿省略)**listing_titles, listing_subtitle, detail_headline, "
"selling_bullets, spec_sidebar_lines, faq, detail_mid_story_paragraphs, usage_and_pairing_tips, "
"short_graphic_post_variants, live_script_bullets, traceability_note, main_image_three_points, "
"live_or_short_hook, customer_service_opening, text_to_image_prompt_main, "
"text_to_image_prompt_scene, text_to_video_prompt。\n"
"**丰富度**:在遵守信息卡前提下尽量写满条数与段落;**禁止**为凑字编造。\n"
"**文生图/视频**:须让「货」和卖点**看得见**(如松软→截面气孔、手撕/按压回弹);禁止整段只有白底健康食品而无具体形态与质地描写。\n"
"输入数据:\n"
+ json.dumps(payload, ensure_ascii=False)
)
raw = call_llm(DETAIL_PACK_SYSTEM, user)
return normalize_detail_page_pack(_parse_llm_json(raw))
def generate_marketing_detail_pack(
*,
keyword: str,
strategy_markdown: str,
strategy_decisions: dict[str, Any] | None = None,
business_notes: str = "",
) -> dict[str, Any]:
core = generate_core_info_card(
keyword=keyword,
strategy_markdown=strategy_markdown,
strategy_decisions=strategy_decisions,
business_notes=business_notes,
)
pack = generate_detail_page_pack(keyword=keyword, core_info_card=core)
return {
"core_info_card": core,
"detail_page_pack": pack,
}