analyze_centrum_transcripts.py 109 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621
  1. import json
  2. import re
  3. import zipfile
  4. import xml.etree.ElementTree as ET
  5. from collections import Counter, defaultdict
  6. from datetime import datetime
  7. from pathlib import Path
  8. BASE = Path("outputs/centrum_douyin_audit_2026-06-08")
  9. LIVE_DIR = BASE / "live-full"
  10. TRANSCRIPT_DIR = BASE / "transcript-full"
  11. LIVE_DATA = LIVE_DIR / "live-analysis-data.json"
  12. RUN_LOG = TRANSCRIPT_DIR / "transcription-run-log.json"
  13. RULE_PACK_DOC = Path(r"E:\新建文件夹\WXWork\1688855615758934\Cache\File\2026-06\QW1_Rule_Pack_ExamplesV0.1.docx")
  14. ACCOUNT_LIST_XLSX = Path(r"E:\新建文件夹\WXWork\1688855615758934\Cache\File\2026-06\店铺与账号.xlsx")
  15. SUPPLEMENTAL_STRATEGY_MD = Path(r"E:\新建文件夹\WXWork\1688855615758934\Cache\File\2026-06\抖音保健品过审黄金公式与边界策略.md")
  16. RUNNER_SUPPLEMENT_JSON = BASE / "runner-supplement-check" / "daily-report.json"
  17. RUNNER_SUPPLEMENT_RAW = BASE / "runner-supplement-check" / "runner-raw-input.json"
  18. PUBLIC_PROFILE_ALL_VIDEOS = BASE / "recollection-2026-06-09" / "attempt-10-public-profile-extract" / "public-profile-videos.json"
  19. PUBLIC_PROFILE_NEW_VIDEOS = BASE / "recollection-2026-06-09" / "attempt-10-public-profile-extract" / "public-profile-new-videos.json"
  20. OUT_JSON = TRANSCRIPT_DIR / "transcript-analysis-data.json"
  21. OUT_MD = TRANSCRIPT_DIR / "善存抖音逐字稿话术审核分析.md"
  22. def clean(value):
  23. return re.sub(r"\s+", " ", str(value or "")).strip()
  24. def load_json(path):
  25. return json.loads(path.read_text(encoding="utf-8"))
  26. def chinese_count(text):
  27. return len(re.findall(r"[\u4e00-\u9fff]", text or ""))
  28. def snippet(text, length=120):
  29. text = clean(text)
  30. return text if len(text) <= length else text[: length - 1] + "…"
  31. NORMALIZE_REPLACEMENTS = [
  32. ("阴阳包", "营养包"),
  33. ("30家", "30+"),
  34. ("40家", "40+"),
  35. ("女40", "女40+"),
  36. ("男40", "男40+"),
  37. ("富维", "复维"),
  38. ]
  39. def normalize_asr(text):
  40. text = clean(text)
  41. for old, new in NORMALIZE_REPLACEMENTS:
  42. text = text.replace(old, new)
  43. return text
  44. RULE_PATTERNS = [
  45. ("RP01", "绝对化/一粒补全/闭眼入", r"闭眼入|就够了|一包全部|一粒.*(就够|补齐|补充到|满足)|全营养|全部.*营养|需要的.*都有|都补充"),
  46. ("RP02", "疾病/治疗/特殊健康时期", r"甲流|感冒|治疗|治好|预防|医生|肿瘤|病"),
  47. ("RP03", "未经确认功效/状态承诺", r"精力|脑力|大脑|气血|代谢|吃瘦|瘦|焕活|状态|活力|疲惫|亏空|底子|皮肤状态|气色|睡眠|抗炎"),
  48. ("RP04", "数字成分推导功效", r"\d+\s*(种|倍|个月|粒|大|天|年)|26种|22种|15倍|40多年|120粒|240粒"),
  49. ("RP05", "海外/跨境/版本边界", r"海外|美国|澳洲|跨境|进口|本土版|Costco|版本|国内"),
  50. ("RP06", "正品/效期/包装信任", r"正品|喷码|批号|效期|包装|保障|大品牌|国际品牌|官方"),
  51. ("RP07", "用法用量/特殊人群", r"每天|一粒|一包|孕妇|儿童|孩子|老人|长辈|爸妈|男士|女士|30\+|40\+|50\+|适合|怎么吃"),
  52. ("RP11", "竞品比较/排名/红黑榜", r"排行榜|排名|只认|不服|红黑榜|智商税|最差|不要买|别乱买|不如|单一的维生素"),
  53. ("RP13", "表达强度过高", r"满格|全开|必须|强烈推荐|真的绝|离不开|快冲|不行|太懂|稳稳当当|直接爱上"),
  54. ("RP14", "证据/专业身份背书不足", r"营养师|医生|研究|实测|专业|干货|建议|国际大品牌|40多年"),
  55. ("RP18", "促销/价格/活动", r"直播间|下单|活动|大促|买一送一|买1送1|拍一发|送|价格|降价|满赠|福利|红包|冲"),
  56. ("RP19", "明星/达人/身份背书", r"秋瓷炫|王艳|百克力|李维嘉|颖儿|明星|达人|他们都在吃|在吃"),
  57. ("RP21", "购买引导/CTA", r"快来|赶紧|下单|直播间|拍|冲|购买|领券|到手价|链接"),
  58. ("RP22", "情绪/趋势钩子", r"焦虑|白月光|趋势|梗王|小妙招|别乱吃|不花冤枉钱|热点|精致女人"),
  59. ]
  60. PUBLIC_TITLE_RISK_PATTERNS = [
  61. ("RP01", "一粒/一瓶解决感", r"一瓶顶多瓶|每天一粒|每日1粒|每日一片|从\s*1\s*粒开始|一粒就够|每天一片|就够了"),
  62. ("RP03", "状态/功效感标题", r"精力|状态|活力|疲惫|少疲惫|亚健康|调理|自救|熬夜|脆皮|补足|营养缺口|内在保养|状态密码|拉满活力"),
  63. ("RP04", "数字成分推导", r"\d+\s*(种|粒|片|岁)|24种|18-50|一瓶|多瓶"),
  64. ("RP05", "海外/跨境版本边界", r"海外|VCHOICE|跨境|进口|版本"),
  65. ("RP06", "正品/渠道信任", r"正品|直发|官方|旗舰店|专卖店|店铺"),
  66. ("RP07", "人群/用法用量", r"男士|男性|老公|男友|打工人|健身党|职场|中年|年轻人|每日|每天|随餐吃"),
  67. ("RP13", "表达强度过高", r"超炸|拉满|硬扛|精准补|实力派|必入|必看|错过再等"),
  68. ("RP18", "促销/价格/活动", r"618|福利|活动|打折|抢跑|羊毛|直播间|链接|赠|大促"),
  69. ("RP21", "强购买 CTA", r"必看|必入|看过来|速来|抓紧|错过再等|薅羊毛"),
  70. ]
  71. MODULE_PATTERNS = [
  72. ("明星/达人背书", r"秋瓷炫|王艳|百克力|李维嘉|颖儿|他们都在吃|达人|明星"),
  73. ("年龄/人群场景", r"30\+|40\+|50\+|女生|女性|男士|中老年|孕妇|儿童|长辈|爸妈|上班族|当妈"),
  74. ("功效/状态表达", r"精力|脑力|大脑|气血|代谢|吃瘦|焕活|状态|活力|气色|疲惫|亏空|底子|抗炎"),
  75. ("成分/剂型说明", r"维生素|矿物质|维矿|叶酸|烟酰胺|生物素|钙|铁|锌|硒|黄芪|大枣|儿茶素|胶原蛋白|谷胱甘肽|颗|粒|包"),
  76. ("一包/一粒解决感", r"一包|一粒|就够|都有|全部|补齐|满足|不费心|不用搭配|瓶瓶罐罐"),
  77. ("版本/渠道信任", r"海外|跨境|美国|澳洲|进口|本土版|官方|正品|版本|大品牌|国际"),
  78. ("促销/购买引导", r"直播间|下单|活动|买|送|拍|价格|福利|满赠|快冲|领券|到手价"),
  79. ("比较/榜单/避坑", r"排行榜|排名|只认|红黑榜|智商税|不要买|别乱买|不花冤枉钱|最差"),
  80. ]
  81. REWRITE_TEMPLATES = {
  82. "明星/达人背书": "达人分享仅代表个人体验;产品信息以包装、标签和官方说明为准。",
  83. "年龄/人群场景": "不同年龄/人群适用产品不同,请按具体 SKU、包装标签和自身需求选择。",
  84. "功效/状态表达": "改为日常营养支持、均衡饮食和规律作息场景,不承诺精力、气血、代谢、焕活等结果。",
  85. "成分/剂型说明": "可保留成分数量和剂型信息,但避免推导为全部所需、明显改善或特定健康结果。",
  86. "一包/一粒解决感": "改为“按标签建议作为日常营养补充”,避免“一包解决/一粒补全”。",
  87. "版本/渠道信任": "不同版本包装、成分和适用说明可能不同,请以购买页、实物标签和客服说明为准。",
  88. "促销/购买引导": "活动、价格、赠品和库存以当前页面为准,CTA 不与功效承诺绑定。",
  89. "比较/榜单/避坑": "避免排名、红黑榜和竞品贬损,改为中性选购维度:成分、剂型、标签、预算、适用人群。",
  90. }
  91. FULL_RULE_NAMES = {
  92. "RP01": "绝对化 / 保证性 / 最高级表达",
  93. "RP02": "疾病治疗 / 治愈 / 诊断暗示",
  94. "RP03": "未经确认功效 / 状态承诺",
  95. "RP04": "效果数据 / 成分数字推导 / 前后对比",
  96. "RP05": "产品法规属性 / 跨境版本边界",
  97. "RP06": "标签一致性 / 用法用量 / 包装信息",
  98. "RP07": "特殊人群 / 用法用量 / 适用范围",
  99. "RP08": "政府/监管/官方背书暗示",
  100. "RP09": "恐惧营销 / 歧视弱势人群 / 制造焦虑",
  101. "RP10": "第三方素材 / 肖像 / IP 未授权",
  102. "RP11": "竞品贬损 / 排名 / 红黑榜",
  103. "RP12": "隐私泄露 / 订单与评论截图",
  104. "RP13": "表达强度过高 / 结果确定性",
  105. "RP14": "研究证据 / 专业身份背书不足",
  106. "RP15": "孕妇儿童老人等特殊人群强推荐",
  107. "RP16": "平台禁用词 / 谐音规避",
  108. "RP17": "口播字幕画面不一致 / A 产品 B 卖点",
  109. "RP18": "促销 / 价格 / 活动 / 库存表达",
  110. "RP19": "明星达人专家身份背书",
  111. "RP20": "低俗擦边 / 品牌安全 / 公序良俗",
  112. "RP21": "购买压力 / 强 CTA / 情绪化催单",
  113. "RP22": "公共事件 / 灾难疾病恐慌 / 极端钩子",
  114. "RP23": "平台反馈原因不明 / 候选风险",
  115. "RP24": "待确认监管新规 / 候选风险",
  116. "RP25": "历史投诉高发模式 / 候选风险",
  117. "RP26": "AI 合成肖像 / 声音 / 商业使用",
  118. "RP27": "新兴平台风险 / DP 反馈待确认",
  119. "RP28": "Out of Scope / 自动审批发布等非本轮范围",
  120. }
  121. LEGAL_REFERENCES = [
  122. {
  123. "name": "《中华人民共和国广告法》",
  124. "url": "https://www.gov.cn/guoqing/2021-10/29/content_5647620.htm",
  125. "use": "广告真实合法、绝对化用语、疾病治疗功能、保健食品广告限制、广告代言与审查底线。",
  126. },
  127. {
  128. "name": "《中华人民共和国食品安全法》",
  129. "url": "https://www.nhc.gov.cn/fzs/c100048/201808/31b2202291464a5fb9e1f64bebc4d877.shtml",
  130. "use": "保健食品标签说明书、适宜/不适宜人群、功效成分、广告审查批准和“不能代替药物”。",
  131. },
  132. {
  133. "name": "《药品、医疗器械、保健食品、特殊医学用途配方食品广告审查管理暂行办法》",
  134. "url": "https://www.gov.cn/gongbao/content/2020/content_5488918.htm",
  135. "use": "三品一械广告发布前审查、保健食品广告内容以注册证书/备案凭证和说明书为准、审查通过内容一致性。",
  136. },
  137. {
  138. "name": "市场监管总局 2026 年三品一械广告审查管理办法征求意见稿",
  139. "url": "https://www.samr.gov.cn/hd/zjdc/art/2026/art_2a99498546434468b1fada4203fdc7ce.html",
  140. "use": "提示三品一械广告审查口径仍在更新,正式定稿需以最新监管文件和法务确认版本为准。",
  141. },
  142. {
  143. "name": "《互联网广告管理办法》",
  144. "url": "https://www.gov.cn/zhengce/202305/content_6858084.htm",
  145. "use": "用于互联网广告可识别性、变相广告、依法需审查广告发布前审查和达人电商场景的边界判断。",
  146. },
  147. {
  148. "name": "GB 28050《预包装食品营养标签通则》(2011/2025 衔接口径)",
  149. "url": "https://www.nhc.gov.cn/sps/c100087/202509/470fa4ff5de14dd38619223cce9da4e7.shtml",
  150. "use": "营养标签、营养声称和营养成分功能/作用声称需匹配标准用语;2025 版实施前后需由法务确认适用版本。",
  151. },
  152. ]
  153. SPEECH_SYSTEM_ROWS = [
  154. [
  155. "功效描述",
  156. "精力满格、气血变好、代谢提升、吃了变瘦、皮肤状态变好",
  157. "作为日常营养补充;配合均衡饮食和规律作息;具体感受因人而异",
  158. "《广告法》第十八条禁止保健食品广告作功效、安全性断言或保证;超出批文/备案范围的功能 claim 需截停",
  159. "营养包样本里“状态/代谢/气血”多为边界表达,达人 brief 应弱化为生活场景。",
  160. ],
  161. [
  162. "营养成分功能声称",
  163. "某成分促进/修复/激活身体状态,或把国标语改成更强同义词",
  164. "仅在符合标签与标准条件时使用 GB 28050 对应标准用语原文",
  165. "GB 28050 对营养声称、营养成分功能/作用声称有标准用语和条件要求;不得自行放大",
  166. "“26 种/22 种”可作为标签事实,不能接成“一粒补全/全部所需”。",
  167. ],
  168. [
  169. "数字推导",
  170. "一粒等于很多瓶、一次补齐、一天营养都够、含量翻倍吸收",
  171. "每份/每粒/每包含有若干营养素;以商品页、包装标签和检测/备案资料为准",
  172. "《广告法》第十一条要求广告中数据真实、准确并表明出处;数字不得推导不可证功效",
  173. "多维 SKU 保留成分数量,但删除“满足全部/闭眼入/就够了”。",
  174. ],
  175. [
  176. "适用人群",
  177. "大人小孩都能吃、所有女生都需要、爸妈必须囤、孕妇老人也适合",
  178. "适用人群、用法用量以具体 SKU 标签、说明书和客服说明为准",
  179. "保健食品广告内容不得超出注册证书/备案凭证、说明书和标签范围",
  180. "30+/40+/50+ 是选购入口,不是全人群适用承诺。",
  181. ],
  182. [
  183. "疾病/特殊健康时期",
  184. "甲流、感冒、肿瘤、术后、治疗、预防、改善症状",
  185. "一般营养补充信息;特殊健康状况请咨询专业人士",
  186. "《广告法》第十七条、第十八条禁止食品/保健食品涉及疾病预防、治疗功能",
  187. "已发布疾病词只作为撞审边界记录,不进入可复制话术库。",
  188. ],
  189. [
  190. "专业/明星背书",
  191. "医生推荐、营养师强推、明星都在吃所以适合你",
  192. "达人/明星分享仅代表个人体验;产品信息以包装标签、商品页和官方说明为准",
  193. "保健食品广告不得利用广告代言人作推荐、证明,医务人员身份风险更高",
  194. "秋瓷炫、颖儿、李维嘉等样本可看传播结构,不直接复制背书逻辑。",
  195. ],
  196. [
  197. "促销 CTA",
  198. "赶紧冲、手慢无、再不买就亏、下单解决状态问题",
  199. "活动、赠品、价格和库存以当前页面为准;按需选择",
  200. "互联网广告与平台虚假宣传规则要求价格、库存、优惠真实,不制造虚假紧迫感",
  201. "官方店铺可承接活动规则,达人强 CTA 标为边界样本。",
  202. ],
  203. [
  204. "跨境/版本",
  205. "海外版更有效、国内外通用功效、进口所以更适合",
  206. "不同版本包装、成分、标签、售后和适用说明可能不同,请以商品页和实物标签为准",
  207. "跨境商品不能自动套用国内蓝帽、保健食品批文或国内标签功能口径",
  208. "善存海外/Costco/澳洲版样本重点做版本信任和客服承接。",
  209. ],
  210. ]
  211. CORE_JUDGMENT_MATRIX_ROWS = [
  212. ["备案/批文内保健功能", "默认红灯,资料齐全后可转绿", "可说,需蓝帽/备案/广审/提示语一致", "不可代言推荐", "产品批文、说明书、广告审查内容、商品页和口播一致", "需批文/广审核验后复制"],
  213. ["营养成分功能声称", "黄灯", "可有限使用标准用语", "只能做中性科普,避免品牌推荐", "匹配 GB 28050 与产品标签;不得同义放大", "需标签/法务核验"],
  214. ["成分/规格事实", "绿灯基础层", "可说", "可说", "数字、规格、剂型可在包装或商品页核验", "高可复制"],
  215. ["个人食用体验", "黄灯灰区", "可说非功效体验", "严格限制", "仅限口感、包装、携带、便利性;不得涉及身体变化", "达人需弱化"],
  216. ["状态/功效感", "红灯或高边界", "仅在批文/广审支持时可控表达", "不可直接说", "精力、气血、代谢、睡眠、皮肤等需逐条核验", "边界样本"],
  217. ["疾病预防/治疗", "红灯截停", "不可说", "不可说", "含疾病、诊断、治疗、预防、症状改善即截停", "不可复制"],
  218. ["促销/购买引导", "黄灯", "可说真实活动", "弱 CTA", "活动信息与页面一致,且不绑定功效承诺", "官方可复制,达人慎用"],
  219. ]
  220. COMPLIANCE_PRINCIPLE_ROWS = [
  221. {
  222. "title": "原则1|强制提示语原则",
  223. "content": "凡涉及保健食品广告/推广,应显著提示“保健食品不是药物,不能代替药物治疗疾病”。提示语不能抵消违法功效、疾病治疗或达人代言推荐风险。",
  224. "check": "发布前核验标题、口播、字幕、画面、商品卡、落地页和评论区是否都没有把保健食品当药品表达。",
  225. "evidence": "当前善存稿需客户补齐具体 SKU 的蓝帽/备案范围、广审批件与页面提示语位置。",
  226. },
  227. {
  228. "title": "原则2|标签一致原则",
  229. "content": "所有成分数量、适用人群、用法用量、版本差异、营养声称和功能声称都必须回到具体 SKU 标签、说明书、商品页和批文。",
  230. "check": "同一视频不得出现口播 A SKU、画面 B SKU、商品卡 C SKU、评论区 D 功效的混配。",
  231. "evidence": "善存营养包、多维、海外版、50+、小佳维等不能互相套用人群和功能口径。",
  232. },
  233. {
  234. "title": "原则3|资质匹配原则",
  235. "content": "同一句话在官方/店铺账号、达人账号、医生/营养师账号、分销/跨境账号下的审核上限不同;达人不能继承品牌官方的广告审查能力。",
  236. "check": "先判断账号身份,再判断产品属性,再判断 claim 层级;没有账号与产品资质证据时,不把已发布样本写成绿灯。",
  237. "evidence": "本轮用 `店铺与账号.xlsx` 匹配官方/店铺账号,未命中清单的店铺型账号仍标为待核验。",
  238. },
  239. ]
  240. ACCOUNT_CLAIM_STRATEGY_ROWS = [
  241. ["官方/店铺账号", "Level 0-1 生活/成分事实", "直接可用", "用标签、商品页、客服和页面活动承接"],
  242. ["官方/店铺账号", "Level 2 营养成分功能声称", "按标准用语谨慎使用", "需 GB 28050、标签和法务口径逐字校验"],
  243. ["官方/店铺账号", "Level 3 备案内保健功能", "资料齐全后可用", "需蓝帽/备案/广审/提示语/页面一致"],
  244. ["官方/店铺账号", "Level 4-5 备案外功能/疾病", "截停或改写", "改为日常营养支持、标签事实或专业咨询"],
  245. ["达人/内容账号", "Level 0-0.5 生活/体验", "可用但弱化", "只说个人习惯、便利性、口感、场景,不说身体变化"],
  246. ["达人/内容账号", "Level 1 成分事实", "可做中性选购", "不推荐具体功效,不强 CTA,不替代官方说明"],
  247. ["达人/内容账号", "Level 2-3 功能声称", "高风险", "一般不建议说品牌功能;转为标签阅读或客服入口"],
  248. ["医生/营养师账号", "任何品牌推荐", "高风险", "科普与产品推广分离,不用专业身份证明具体 SKU"],
  249. ]
  250. PRODUCT_PERSON_STRATEGY_ROWS = [
  251. ["善存营养包/PRO", "30+/忙碌/熬夜/身材管理人群", "省心搭配、一包便利、日常补充", "代谢、吃瘦、气血、皮肤变好", "弱化为生活场景和按标签补充"],
  252. ["善存复合维生素/多维", "成人男女版", "成分数量、剂型、规格、每日习惯", "一粒补全、满足全部所需", "成分事实 + 标签回流"],
  253. ["善存女性/男士/50+", "分龄分性别人群", "标签适用范围、版本差异", "所有女生/男士都适合、爸妈必须吃", "按具体 SKU 人群说明,不跨 SKU 套用"],
  254. ["海外/跨境版本", "海淘/正品/版本关注人群", "包装、批号、效期、渠道、客服", "海外更有效、国内外通用功能", "版本事实 + 售后承接"],
  255. ["小佳维/儿童相关", "儿童/家长", "儿童 SKU 标签和用量说明", "儿童都能吃、长高变聪明", "特殊人群只走标签与专业咨询"],
  256. ["银善存/大脑类素材", "40+/50+/长辈", "画面隐喻、生活任务、标签信息", "改善记忆、提升脑力、治疗认知问题", "备案内才可直接说,其他转隐喻/场景"],
  257. ]
  258. SELLING_POINT_REWRITE_ROWS = [
  259. ["状态/精力/气血", "精力满格、气血满格、状态全开", "日常营养支持;配合规律作息;保持好状态的生活习惯之一", "《广告法》第十八条", "达人、官方都需慎用"],
  260. ["代谢/身材", "代谢包、吃瘦、身材管理靠它", "作为日常营养补充选择之一,不替代饮食管理和运动", "不得暗示减肥/治疗/确定改善", "达人强风险"],
  261. ["成分数量", "一粒补充 26 种,全部都够", "含多种维生素和矿物质,具体成分以包装标签为准", "广告数据真实准确;GB 28050 标签口径", "全账号可用,但不得推导"],
  262. ["一包省心", "一包解决所有营养、懒人闭眼入", "独立小包/组合搭配,便于按标签建议补充", "绝对化/保证性表达风险", "营养包核心可复制卖点"],
  263. ["明星/达人", "明星都在吃,你也快冲", "达人分享仅代表个人体验,产品信息以官方说明为准", "保健食品广告代言限制", "达人/明星样本需授权复核"],
  264. ["跨境/正品", "海外版更好,进口更有效", "不同版本包装、成分和售后政策可能不同,以商品页和实物标签为准", "版本与资质不得混用", "跨境 SKU 必备"],
  265. ["促销活动", "手慢无、再不买就没了", "当前活动以页面展示为准,按需选择", "虚假紧迫感/价格库存真实性", "官方可用,达人弱化"],
  266. ["疾病词", "甲流/感冒/肿瘤/治疗/预防", "特殊健康状况请咨询专业人士;本内容仅作一般营养信息", "《广告法》第十七条、第十八条", "不可复制"],
  267. ]
  268. AI_FORMULA_GROUPS = [
  269. {
  270. "title": "公式组A:话术改写类(可直接编码为规则引擎)",
  271. "items": [
  272. "A1 功效词替换:IF 文本含精力/气血/代谢/皮肤状态/睡眠改善 THEN 改为日常营养支持 + 个体差异 + 标签依据。",
  273. "A2 疾病词截停:IF 文本含疾病名/治疗/预防/诊断/症状改善 THEN 标为红灯,不进入可复制话术。",
  274. "A3 数字推导降级:IF 数字后接补全/翻倍/满足全部/相当于 THEN 保留标签数字,删除效果推导。",
  275. "A4 人群泛化回流:IF 文本含全家/所有人/爸妈都适合/儿童孕妇 THEN 回到具体 SKU 标签和专业咨询。",
  276. "A5 GB 28050 校验:IF 文本含营养成分功能/作用声称 THEN 校验是否与适用标准用语和标签条件一致。",
  277. ],
  278. },
  279. {
  280. "title": "公式组B:账号差异化规则",
  281. "items": [
  282. "B1 官方/店铺账号 = 产品资质 + 广审范围 + 商品页一致 + 强制提示语;缺任一项则降为待核验。",
  283. "B2 达人账号 = 个人体验 + 生活场景 + 成分事实 + 弱 CTA;不得承接官方备案功能或代言证明。",
  284. "B3 医生/营养师账号 = 科普与具体 SKU 分离;不得用专业身份推荐善存某 SKU。",
  285. "B4 跨境/分销账号 = 版本事实 + 渠道承接;不得套用国内蓝帽或官方旗舰店能力。",
  286. ],
  287. },
  288. {
  289. "title": "公式组C:内容架构规则",
  290. "items": [
  291. "C1 稳妥脚本结构 = 生活场景 Hook + 成分/规格事实 + 标签/商品页承接 + 弱购买入口。",
  292. "C2 边界脚本结构 = 强情绪 Hook + 状态隐喻 + 明星/达人场景 + 促销承接;必须逐条审核。",
  293. "C3 评论承接结构 = 置顶 FAQ + 功效追问统一回复 + 版本/正品/用量分流客服。",
  294. "C4 多模态一致性 = 标题、口播、字幕、画面、商品卡、评论区不得互相补违规 claim。",
  295. ],
  296. },
  297. {
  298. "title": "公式组D:宣称层次自动判定",
  299. "items": [
  300. "D0 生活场景:只含日常习惯/携带/包装/口感,默认绿灯基础层。",
  301. "D1 成分规格:含成分名、数量、剂型、包装规格,需标签可核验。",
  302. "D2 标准声称:含营养成分功能/作用声称,需 GB 28050 与标签条件校验。",
  303. "D3 备案内保健功能:仅官方/店铺在批文和广审一致时可表达。",
  304. "D4 备案外功效感:精力、气血、代谢、脑力、睡眠、抗炎等默认红灯或边界。",
  305. "D5 疾病治疗:疾病、治疗、预防、诊断、症状改善,一律截停。",
  306. ],
  307. },
  308. {
  309. "title": "公式组E:三维决策矩阵(完整版)",
  310. "items": [
  311. "E1 账号维度:官方/店铺、达人、医生/营养师、分销/跨境分别判定上限。",
  312. "E2 产品维度:保健食品、普通食品、跨境版本、OTC/药品先分流。",
  313. "E3 Claim 维度:生活场景、成分事实、标准声称、备案内功能、备案外功能、疾病治疗分层。",
  314. "E4 承载维度:口播、字幕、画面、商品卡、评论区、直播间任一处补违规 claim 都要回流审核。",
  315. "E5 复用结论:高可复制、需资料核验、边界样本、不可复制四类输出到黄金数据集和 Excel。",
  316. ],
  317. },
  318. ]
  319. SELF_CHECK_ROWS = [
  320. ["1", "是否出现疾病名、症状、治疗、预防、诊断暗示?", "出现即红灯,改为一般营养信息或专业咨询。"],
  321. ["2", "是否把精力、气血、代谢、脑力、睡眠、皮肤状态说成确定结果?", "降级为生活场景和日常营养支持。"],
  322. ["3", "是否有“一粒/一包就够、全部补齐、闭眼入”等绝对化表达?", "保留规格事实,删除补全和保证性结论。"],
  323. ["4", "营养成分功能/作用声称是否匹配 GB 28050 与产品标签条件?", "不匹配则改为成分事实或交法务确认。"],
  324. ["5", "适用人群是否回到具体 SKU 标签?", "删除全家都能吃、所有女生/男士都适合等泛化话术。"],
  325. ["6", "账号是否具备对应发布资质?", "官方/店铺、达人、医生/营养师、分销/跨境分开判定。"],
  326. ["7", "官方功能 claim 是否具备蓝帽/备案/广审/强制提示语/页面一致?", "缺资料则标为需批文/广审核验后复制。"],
  327. ["8", "达人内容是否在推荐、证明产品功效?", "改成个人生活习惯和非功效体验,不强导购。"],
  328. ["9", "明星、医生、营养师、研究证据是否可核验并适用于该 SKU?", "无法补证时删除背书,只保留一般信息。"],
  329. ["10", "海外/跨境/进口版本是否套用了国内蓝帽或其他 SKU 口径?", "改为版本、包装、效期、售后和商品页说明。"],
  330. ["11", "价格、赠品、库存、限时活动是否与页面实时一致?", "不确定则写“以当前页面为准”,不制造紧迫感。"],
  331. ["12", "标题、口播、字幕、画面、商品卡和评论区是否一致?", "发现 A 产品 B 卖点 C 功效时回炉修改。"],
  332. ["13", "评论区是否追加了口播里没说的功效承诺?", "统一用 FAQ/客服分流,不在评论区补火。"],
  333. ["14", "是否有红黑榜、智商税、竞品贬损或排名压制?", "改为中性选购维度和自身标签事实。"],
  334. ["15", "是否已给每条样本标注复用结论?", "输出高可复制、需资料核验、边界样本、不可复制。"],
  335. ]
  336. PRODUCT_TYPE_BOUNDARY_ROWS = [
  337. ["保健食品/蓝帽", "国食健字/国食健注、备案或注册凭证", "仅限批准或备案范围内;需强制提示语和广审一致性", "官方/店铺账号可在资料齐全时表达;达人不可代言推荐。"],
  338. ["普通食品", "SC 生产许可、普通食品标签", "不能使用保健功能、疾病改善、治疗预防等表达", "只能讲配料、口味、食用场景、包装和一般生活方式。"],
  339. ["OTC/药品", "国药准字", "不纳入普通达人带货话术;需按药品广告与平台规则单独审查", "本报告仅作为排除项,不把药品话术迁移到善存营养包。"],
  340. ["跨境/海外版", "海外标签、跨境商品信息、无国内蓝帽时需标版本差异", "不能套用国内蓝帽和国内保健功能口径", "只能讲版本、包装、渠道、标签差异和客服承接。"],
  341. ]
  342. ACCOUNT_TRACK_ROWS = [
  343. ["蓝V/官方/店铺账号", "企业认证、店铺归属清晰、产品资质和广审资料可核验", "商品事实、备案内保健功能、价格活动、客服承接", "超出备案范围、跨 SKU 套用、绝对化、疾病治疗、虚假紧迫感"],
  344. ["蓝V海外/跨境店铺", "跨境资质、版本标签、售后和商品页清晰", "海外标签事实、版本差异、包装规格、购买渠道", "把海外版等同国内蓝帽;暗示更有效或国内外通用功效"],
  345. ["达人/KOL账号", "不承接保健食品广告发布主体资格", "生活场景、成分事实、中性选购、口感/包装/便利性体验", "功效推荐、身体变化、强购买引导、代言式证明"],
  346. ["医生/营养师/专家达人", "身份会放大医疗建议和权威背书风险", "一般科普、标签阅读方法、非品牌化知识", "推荐具体品牌/SKU、医疗机构背书、跨专业建议、诊疗暗示"],
  347. ]
  348. CLAIM_LEVEL_ROWS = [
  349. ["Level 0", "生活场景", "我日常会按标签补充;出门携带方便", "绿灯基础层", "一旦延伸到身体变化就升级为功效风险。"],
  350. ["Level 0.5", "食用/使用体验", "颗粒大小、口感、包装、吞咽便利", "达人灰区", "只能讲口感和便利性,不讲精力、气色、睡眠、疼痛等身体变化。"],
  351. ["Level 1", "成分/规格事实", "含多种维生素矿物质;每包含 X 类营养素", "可复制", "数字可说为标签事实,不能推导为补全、改善或替代饮食。"],
  352. ["Level 2", "营养成分功能声称", "某营养素有助于某生理功能", "需国标/标签原文", "必须以适用国标、标签和法务确认口径为准,不自行改写放大。"],
  353. ["Level 3", "备案内保健功能", "已备案或广审范围内的保健功能", "仅官方/店铺资料齐全时可说", "达人不能以个人身份推荐或证明。"],
  354. ["Level 4", "备案外保健功能", "改善记忆力、焕活、代谢提升等超范围 claim", "红灯", "任何账号都不应直接说;只可观察隐喻边界。"],
  355. ["Level 5", "疾病预防/治疗", "甲流、感冒、治疗、预防、诊断、疼痛改善", "红灯截停", "即使已发布也只记录为撞审样本,不作为可复制经验。"],
  356. ]
  357. BOUNDARY_TECHNIQUE_ROWS = [
  358. ["概念替换", "疾病/医疗词 -> 营养学/健康维护词", "甲流/感冒 -> 换季健康管理;治疗/改善 -> 日常营养支持", "中低", "善存样本中疾病词较少,后续需补采专题验证。"],
  359. ["比喻暗示", "功效不写入口播,转为画面或角色隐喻", "银善存大脑球场、守门员/前锋", "中", "只能作为边界样本,不能把隐喻翻译成直接功效。"],
  360. ["权威归因拆解", "荣誉归科学发现,不归产品效果", "国际品牌/40多年历史只能承载品牌信任,不能替代功效证据", "中", "善存样本多用“大品牌/国际品牌/40多年”,需要补证或回到品牌事实。"],
  361. ["成分事实", "只说标签可核验内容,不说产品做到什么", "26种/22种/一包/一粒作为规格事实", "低", "善存多维和营养包样本命中最多,可复制价值高。"],
  362. ["专利/技术事实", "可以说有技术或配方事实,不能说技术带来效果倍数", "升级配方、重点复配、男女版配比只讲事实", "中", "“配比/升级”不能接成“更有效/更适合所有人”。"],
  363. ["个人体验叙事", "第三人称功效声明 -> 第一人称生活习惯 + 免责", "我日常会备/按标签补充/方便坚持", "中", "达人可以用,但不能讲身体变化或引导购买。"],
  364. ["标签回流", "人群/用量/版本一律回到标签、商品页、客服", "30+/40+/50+、男女版、海外版", "低", "适合做置顶评论和客服 FAQ。"],
  365. ["身份脱敏", "专家/医生/明星不作为功效证明", "达人分享仅代表个人体验", "中", "医生专家达人仍属高风险,建议品牌化内容与科普内容分离。"],
  366. ["短脚本冲量", "短促销脚本保留强 Hook", "快冲、手慢无、直播间下单", "高", "可以解释已发布样本,但不建议沉淀为稳定话术。"],
  367. ["隐性比较", "不点名竞品,只讲自身维度", "不同产品各有特点,按成分/剂型/预算选择", "中", "避免红黑榜、智商税、别买某品牌。"],
  368. ]
  369. SYSTEM_FUNCTION_ROWS = [
  370. ["资料接入", "读取 Rule Pack、客户理想稿、官方账号清单、产品批文/标签/广审资料", "形成项目规则底座和账号白名单,不靠模型空猜。"],
  371. ["抖音采集", "按关键词、账号、SKU、claim 缺口采集视频和评论", "区分官方/达人/分销账号,保留已发布成功案例证据。"],
  372. ["逐字稿转写", "优先转高风险、高互动、官方账号、边界 claim 样本", "把标题/标签风险推进到真实口播、字幕和评论诱因分析。"],
  373. ["规则命中", "逐条命中 RP、宣称层次、账号类型、产品属性、复用判断", "输出黄金边界数据集,而不是单条改写建议。"],
  374. ["脚本建议", "按账号和 claim 生成可复制、需核验、边界样本、不可复制四类动作", "同一卖点在蓝V和达人账号下自动给不同上限。"],
  375. ["评论承接", "识别功效追问、版本信任、购买路径、用法人群、体验副作用", "同步生成置顶评论、客服 FAQ 和禁回复口径。"],
  376. ["复盘闭环", "记录采集失败、转写失败、未命中 RP 和补采方向", "每轮报告都能说明样本覆盖与下一轮数据缺口。"],
  377. ]
  378. STRATEGY_FORMULA_ROWS = [
  379. {
  380. "formula": "官方/店铺账号 × 善存营养包/PRO × 省心搭配 × 30+/忙碌人群 × 日常营养支持 × 商品页/标签承接 × 弱 CTA",
  381. "condition": "账号归属清晰,产品标签、商品页、字幕和口播一致。",
  382. "experience": "可把“一包/多种营养/方便坚持”作为便利性卖点,但不要推导成“全部都补齐、状态一定变好”。",
  383. "reuse": "高可复制;适合官方 brief 和店铺短视频。",
  384. },
  385. {
  386. "formula": "达人账号 × 善存营养包/PRO × 一包省心 × 护肤/身材/工作状态 × 状态感 claim × 个人体验 × 无强 CTA",
  387. "condition": "达人只做生活化分享,不承接官方广告备案能力。",
  388. "experience": "把卖点落在“我日常会备、懒人省心、按标签补充”,避免“吃瘦、气血好、皮肤变好、快冲下单”。",
  389. "reuse": "可复制但需弱化;适合达人 brief。",
  390. },
  391. {
  392. "formula": "官方/店铺账号 × 善存复合维生素/多维 × 26/22 种成分 × 成人男女版 × 泛营养 claim × 成分事实 × 商品页承接",
  393. "condition": "成分数量、规格、适用说明能在包装或商品页核验。",
  394. "experience": "数字可以说成“含有”,不要说成“一粒满足全部所需、一次补全、闭眼入”。",
  395. "reuse": "高可复制;适合形成标准商品信息话术。",
  396. },
  397. {
  398. "formula": "达人账号 × 善存复合维生素/多维 × 每天一粒 × 成人男女版 × 便利性 claim × 个人习惯 × 低购买压力",
  399. "condition": "达人表达为个人选择,不做全人群适用判断。",
  400. "experience": "可说“我会看复配、品牌、颗粒大小、性价比”,再回到“按标签选择”;不要说所有女生/男生都需要。",
  401. "reuse": "可复制;注意删除绝对化和强导购。",
  402. },
  403. {
  404. "formula": "官方/店铺账号 × 海外/跨境 SKU × 正品/版本 × 海淘用户 × 信任 claim × 包装/批号/效期 × 客服承接",
  405. "condition": "跨境版本、商品资质、售后入口明确。",
  406. "experience": "海外/进口/本土版只承载渠道和版本事实,不承载“更有效、更适合国内人群”。",
  407. "reuse": "高可复制;适合置顶评论和商品卡 FAQ。",
  408. },
  409. {
  410. "formula": "达人/分销账号 × 海外/Costco/澳洲版 × 版本比较 × 成人/50+ × 功效差异 claim × 选购经验 × 中性比较",
  411. "condition": "账号非官方或授权未核验时,版本信息只能做经验观察。",
  412. "experience": "可说包装、规格、购买场景和个人偏好;不要把不同版本差异说成功效优劣或国内外通用保健功能。",
  413. "reuse": "边界经验;需要人工复核。",
  414. },
  415. {
  416. "formula": "官方/店铺账号 × 银善存/50+ × 大脑/脑力素材 × 中老年/爸妈 × 备案内功能 claim × 官方素材 × 审查一致",
  417. "condition": "必须核验产品批文、说明书、广告审查批件和画面素材。",
  418. "experience": "若 claim 在备案和广审范围内,可相对直接;若不在范围内,只能转为营养支持、生活场景或画面隐喻。",
  419. "reuse": "需批文/广审核验后复制。",
  420. },
  421. {
  422. "formula": "达人账号 × 银善存/大脑类 × 守门员/前锋/球场隐喻 × 40+/脑力场景 × 大脑表现 claim × 画面比喻 × 无医学承诺",
  423. "condition": "直接说“改善记忆/提升脑力/治疗认知问题”风险高。",
  424. "experience": "已发布样本的边界价值在于用角色、运动、任务感承载“状态感”,不把隐喻翻译成医学或功能承诺。",
  425. "reuse": "高边界;逐条审核后才可复用。",
  426. },
  427. {
  428. "formula": "达人/明星账号 × 善存营养包 × 明星在吃 × 粉丝/女性 × 效果背书 claim × 场景分享 × 非功效背书",
  429. "condition": "明星达人内容容易被用户理解为功效或适用性背书。",
  430. "experience": "可保留生活场景和个人体验,不把“他们都在吃”写成“所以你也适合/一定有效”。",
  431. "reuse": "边界样本;必须补授权和背书合规判断。",
  432. },
  433. {
  434. "formula": "官方/店铺账号 × 明星素材 × 多 SKU × 品牌信任 × 背书 claim × 官方授权素材 × 商品页一致",
  435. "condition": "素材授权、代言关系、广告审查内容和商品链接需要一致。",
  436. "experience": "官方账号可以承接品牌信任,但明星素材仍不能替代产品功效证据。",
  437. "reuse": "需素材授权和法务确认。",
  438. },
  439. {
  440. "formula": "任意账号 × 代谢包/身材管理 × 代谢/吃瘦 × 女性/管理期 × 减重结果 claim × 生活方式组合 × 无结果承诺",
  441. "condition": "代谢、吃瘦、身材管理容易被理解为减肥或治疗效果。",
  442. "experience": "把表达转成日常营养补充、均衡饮食和运动作息的一部分,不说“代谢变快、吃了瘦”。",
  443. "reuse": "不可直接复制;需大幅弱化。",
  444. },
  445. {
  446. "formula": "官方/店铺账号 × 直播间活动 × 价格/赠品 × 已关注人群 × 购买信息 claim × 活动规则 × 页面承接",
  447. "condition": "活动信息必须与页面实时一致。",
  448. "experience": "可说活动、赠品、库存和到手价,但不能绑定功效承诺,也不能制造虚假紧迫感。",
  449. "reuse": "可复制;需运营实时校验。",
  450. },
  451. {
  452. "formula": "达人账号 × 橱窗/分销 × 快冲/下单 × 泛人群 × 强购买 CTA × 视频口播 × 评论承接",
  453. "condition": "达人账号的强导购审核边界明显更紧。",
  454. "experience": "即便已发布,也只记录为边界样本;更稳的做法是把购买动作放到商品卡自然承接。",
  455. "reuse": "高风险;不作为标准话术。",
  456. },
  457. {
  458. "formula": "医生/营养师/专业账号 × 任意 SKU × 科普建议 × 特殊健康人群 × 专业背书 claim × 可核验资质 × 不诊断不治疗",
  459. "condition": "专业身份会放大用户对医疗建议的理解。",
  460. "experience": "可做一般营养科普和标签阅读方法,不说诊断、治疗、预防,不把个人身份变成产品功效证明。",
  461. "reuse": "需资质、素材和平台规则复核。",
  462. },
  463. {
  464. "formula": "任意账号 × 孕妇/儿童/老人/慢病人群 × 适用人群 × 特殊人群 × 用量/适合 claim × 标签说明 × 专业咨询",
  465. "condition": "特殊人群不能被一概而论。",
  466. "experience": "把回答固定到具体 SKU 标签、说明书、客服和专业人士;不要说“爸妈都能吃、小孩也适合”。",
  467. "reuse": "慎用;适合 FAQ 而非强口播。",
  468. },
  469. {
  470. "formula": "任意账号 × 多 SKU/竞品比较 × 红黑榜/排名 × 选购人群 × 最好/只认 claim × 中性维度 × 无贬损",
  471. "condition": "红黑榜、排名、智商税和竞品贬损容易触发平台和广告法风险。",
  472. "experience": "改成“看成分、剂型、规格、标签、预算和适用场景”,只讲自身可核验信息。",
  473. "reuse": "可复制中性框架;不可复制贬损表达。",
  474. },
  475. {
  476. "formula": "官方/店铺账号 × 评论区承接 × 功效追问 × 已购买/观望人群 × 效果如何 claim × 标准 FAQ × 客服分流",
  477. "condition": "评论区回复也是广告表达的一部分。",
  478. "experience": "对“效果明显吗”统一回到日常营养补充、均衡饮食、个体差异和专业咨询,不在评论区追加功效承诺。",
  479. "reuse": "高可复制;适合置顶评论模板。",
  480. },
  481. {
  482. "formula": "任意账号 × 疾病/甲流/感冒 × 治疗/预防 × 特殊健康时期 × 医疗 claim × 删除或转咨询 × 无导购",
  483. "condition": "出现疾病、治疗、预防、诊断暗示时,不因已发布就认定可复制。",
  484. "experience": "只能作为撞审边界记录;稳定话术应回到一般营养信息和专业人士咨询。",
  485. "reuse": "不可复制。",
  486. },
  487. ]
  488. UNOBSERVED_RULE_SAMPLING_GUIDE = {
  489. "RP08": "补采含“国家认证、监管推荐、官方背书、平台认证截图、蓝帽等同万能资质”等表述的视频,区分真实资质展示和背书滥用。",
  490. "RP09": "补采制造焦虑或歧视性表达,例如“30+不补就垮、爸妈不吃就危险、熬夜党必须囤”等,观察恐惧钩子能否发布。",
  491. "RP10": "补采明星综艺截屏、影视/IP 素材、音乐音频、他人评论截图和未经授权图文素材样本,建立素材授权边界。",
  492. "RP12": "补采订单、物流、聊天记录、用户评论截图和售后截图类视频,判断隐私遮挡、授权和截图来源要求。",
  493. "RP15": "补采孕妇、儿童、老人、术后、慢病、备孕等特殊人群样本,和 RP07 的用法用量规则联动复盘。",
  494. "RP16": "补采谐音避审、错别字规避、字幕替换禁词、口播不说字幕暗示等样本,判断平台是否按语义识别。",
  495. "RP17": "补采口播 A 产品、画面 B 产品、标题 C 卖点、商品卡 D SKU 的混配样本,验证素材一致性风险。",
  496. "RP20": "补采擦边玩梗、低俗隐喻、过度身材焦虑、攻击性表达等样本,判断品牌安全边界。",
  497. "RP22": "补采公共事件、疾病流行、考试季焦虑、节日恐慌和趋势钩子样本,判断热点借势与恐惧营销边界。",
  498. "RP23": "收集平台驳回、限流、申诉、人工审核反馈和发布时间差异样本,用于反推 DP/平台候选风险。",
  499. "RP24": "跟踪三品一械广告审查新规、平台健康品规则更新和品牌法务口径变化,形成监管候选规则池。",
  500. "RP25": "补采副作用、不适、吞咽困难、投诉、售后争议和夸大宣传质疑评论,沉淀历史投诉高发模式。",
  501. "RP26": "补采 AI 合成明星脸、AI 声音、数字人带货、仿真人设口播样本,核验授权与平台标识要求。",
  502. "RP27": "补采 DP 反馈、平台新增禁投词、新兴达人玩法和特殊账号类型样本,作为新增规则候选。",
  503. "RP28": "该类多为流程或范围外事项,暂不主动补采;仅在平台审核流程、自动审批或发布机制影响结论时记录。",
  504. }
  505. RULE_REWRITE_TEMPLATES = {
  506. "RP01": "删除“唯一、最好、全网第一、就够、补齐、全部所需”等绝对化或保证性表达;改为“含多种维生素/矿物质,可按标签建议作为日常营养补充”。",
  507. "RP02": "删除甲流、感冒、治疗、预防、医生诊断等疾病/医疗暗示;改为“一般营养补充信息,特殊健康状况请咨询专业人士”。",
  508. "RP03": "删除精力满格、气血改善、代谢变好、焕活、抗炎等结果承诺;改为“配合均衡饮食和规律作息,支持日常营养补充”。",
  509. "RP04": "保留可核验的成分数量、规格和剂型,但不要把“26种/22种/120粒”等数字推导成效果、补全或改善结果。",
  510. "RP05": "跨境/海外/不同版本只讲包装、标签、适用说明和购买渠道差异;不要默认等同国内蓝帽、保健功能或统一适用人群。",
  511. "RP06": "正品、批号、喷码、效期、包装等信息以商品页、实物标签和客服确认为准,避免用“大品牌”替代合规凭证。",
  512. "RP07": "按具体 SKU 标签建议说明用法用量和适用人群;孕妇、儿童、老人、30+/40+/50+等人群不要一概而论。",
  513. "RP11": "删除红黑榜、智商税、别买某品牌、排名碾压等竞品贬损;改为中性选购维度和自身产品标签信息。",
  514. "RP13": "弱化“必须、离不开、快冲、稳稳、真的有效”等强语气;改为“可作为日常补充选择之一,按需选择”。",
  515. "RP14": "医生、营养师、专业建议或研究背书需要可核验证据;无法补证时改为个人经验/一般选购建议,并回到标签依据。",
  516. "RP18": "活动、价格、赠品、库存和到手价以当前页面为准,不虚构限量或福利,也不把促销 CTA 与功效承诺绑定。",
  517. "RP19": "明星/达人/专家内容仅作为分享场景,不作为功效或适用性背书;补充“产品信息以包装标签和官方说明为准”。",
  518. "RP21": "删除“马上下单就解决、快冲改善状态”等强 CTA;改为“如需了解规格/适用说明,可查看商品页或咨询客服”。",
  519. "RP22": "避免借焦虑、趋势、恐慌、白月光或梗化表达放大购买压力;改为平实生活场景和可核验产品信息。",
  520. }
  521. RULE_MODULE_MAP = {
  522. "RP01": "一包/一粒解决感",
  523. "RP02": "功效/状态表达",
  524. "RP03": "功效/状态表达",
  525. "RP04": "成分/剂型说明",
  526. "RP05": "版本/渠道信任",
  527. "RP06": "版本/渠道信任",
  528. "RP07": "年龄/人群场景",
  529. "RP11": "比较/榜单/避坑",
  530. "RP13": "功效/状态表达",
  531. "RP14": "版本/渠道信任",
  532. "RP18": "促销/购买引导",
  533. "RP19": "明星/达人背书",
  534. "RP21": "促销/购买引导",
  535. "RP22": "促销/购买引导",
  536. }
  537. COMMENT_REPLY_TEMPLATES = {
  538. "泛互动/情绪反馈": "感谢喜欢,产品信息可查看商品页成分表、包装标签和客服说明。",
  539. "功效/健康诉求": "营养补充品不承诺特定效果,建议结合均衡饮食和规律作息;特殊健康状况请咨询专业人士。",
  540. "正品/渠道/版本信任": "不同版本包装、成分和标签说明可能不同,正品、批号、效期和售后以商品页及实物标签为准。",
  541. "购买路径/价格": "活动、赠品、库存和价格以当前页面为准;如需规格或适用说明,可咨询客服。",
  542. "质疑/比较/性价比": "可从成分、剂型、规格、标签说明、预算和适用人群做中性比较,不建议用红黑榜或贬损表达。",
  543. "内容互动/达人明星": "达人分享仅代表个人体验,产品信息以包装标签、商品页和官方说明为准。",
  544. "低质互动/刷金币": "需人工复核评论上下文后再互动,避免用模板回复强化无效互动。",
  545. "用法用量/适用人群": "不同 SKU 适用人群和建议用量不同,请按包装标签、商品页和客服说明选择。",
  546. "体验/副作用/剂型": "关于服用体验、颗粒大小、吞咽和不适感,可引导查看 FAQ 或咨询客服;不要在评论区给医疗建议。",
  547. }
  548. COMMENT_TRANSCRIPT_TRIGGERS = {
  549. "泛互动/情绪反馈": "明星/达人场景、强生活化表达和高互动标题会带来大量情绪评论,但信息价值较低。",
  550. "功效/健康诉求": "精力、气血、代谢、状态、抗炎、甲流等口播会触发用户追问“效果怎么样”。",
  551. "正品/渠道/版本信任": "海外、跨境、官方、大品牌、国内外版本等表达会触发真假、版本、效期和售后疑问。",
  552. "购买路径/价格": "直播间、拍一发四、送礼袋、活动数量有限、快冲等口播会把用户注意力拉到价格和购买路径。",
  553. "质疑/比较/性价比": "红黑榜、排行榜、只认某款、别乱买等表达会引发比较、质疑和性价比讨论。",
  554. "内容互动/达人明星": "明星都在吃、达人推荐、医生/营养师身份会带来背书追问,也会放大合规风险。",
  555. "低质互动/刷金币": "高流量视频容易混入刷金币、求赞、无意义互动,需和真实疑问分开处理。",
  556. "用法用量/适用人群": "30+/40+/50+、孕妇、儿童、爸妈、长辈等口播会触发适用人群和用量追问。",
  557. "体验/副作用/剂型": "一粒/一包/颗粒/吞咽/搭配方式等口播会触发体验和服用方式问题。",
  558. }
  559. def load_account_list():
  560. records = []
  561. names = set()
  562. if not ACCOUNT_LIST_XLSX.exists():
  563. return {"records": records, "names": names}
  564. try:
  565. import openpyxl
  566. wb = openpyxl.load_workbook(ACCOUNT_LIST_XLSX, data_only=True)
  567. ws = wb.active
  568. for row in ws.iter_rows(min_row=4, values_only=True):
  569. store = clean(row[0] if len(row) > 0 else "")
  570. live_account = clean(row[2] if len(row) > 2 else "")
  571. douyin_id = clean(row[3] if len(row) > 3 else "")
  572. if not store and not live_account:
  573. continue
  574. record = {
  575. "store": store,
  576. "liveAccount": live_account,
  577. "douyinId": douyin_id,
  578. }
  579. records.append(record)
  580. for name in (store, live_account):
  581. if name:
  582. names.add(name)
  583. except Exception:
  584. return {"records": records, "names": names}
  585. return {"records": records, "names": names}
  586. def classify_account(author, official_names):
  587. author = clean(author)
  588. if author in official_names:
  589. return {
  590. "accountType": "官方/店铺账号(清单匹配)",
  591. "accountConfidence": "高",
  592. "accountRuleLogic": "可承接商品资质、广告审查备案和官方说明;功能宣称是否可说,取决于产品属性、批文/备案范围和素材一致性。",
  593. }
  594. if re.search(r"善存.*(旗舰店|官方|专卖店|店铺|直播|营养品)|VCHOICE", author, flags=re.I):
  595. return {
  596. "accountType": "疑似店铺/分销账号(待核验)",
  597. "accountConfidence": "中",
  598. "accountRuleLogic": "可能具备商品承接能力,但不在本次官方清单内;结论需回查账号认证、店铺归属和广告备案。",
  599. }
  600. if re.search(r"旗舰店|官方|专卖店|店铺|优选|海外专营店", author, flags=re.I):
  601. return {
  602. "accountType": "其他店铺/分销账号(非官方清单)",
  603. "accountConfidence": "中",
  604. "accountRuleLogic": "可做购买承接但不应默认等同品牌官方;需要核验授权关系和商品资质。",
  605. }
  606. return {
  607. "accountType": "达人/内容账号",
  608. "accountConfidence": "中",
  609. "accountRuleLogic": "应以个人体验、生活场景和中性科普为主;不得替代官方广告备案,不宜强 CTA 或直接导购。",
  610. }
  611. def account_rule_notes(account_type):
  612. if account_type.startswith("官方/店铺"):
  613. return "官方/店铺账号可在资质与广审备案范围内表达功能或商品信息,但要保证产品、画面、口播、字幕和落地页一致。"
  614. if account_type.startswith("疑似店铺") or account_type.startswith("其他店铺"):
  615. return "店铺/分销账号要先核验授权和资质;在未确认前,不把功能宣称当作绿灯经验,只记录为边界样本。"
  616. return "达人账号的成功经验主要是生活化、隐喻化、弱导购和个人体验化;功能/治疗/强购买引导仍按更严边界处理。"
  617. def claim_boundary_for_row(row):
  618. text = f"{row.get('cleanText', '')} {row.get('title', '')}"
  619. account_type = row.get("accountType", "")
  620. sku = row.get("skuGroup", "")
  621. if re.search(r"大脑|脑力|守门员|前锋", text):
  622. claim = "脑力/大脑表现"
  623. carrier = "画面隐喻或角色比喻优于直接效果承诺"
  624. elif re.search(r"精力|状态|气血|代谢|焕活|抗炎|皮肤", text):
  625. claim = "状态/功效感"
  626. carrier = "生活场景 + 日常营养支持,避免直接承诺改善结果"
  627. elif re.search(r"甲流|感冒|疾病|治疗|预防", text):
  628. claim = "疾病/特殊健康时期"
  629. carrier = "不作为成功经验复制;只能回到一般营养补充或专业咨询"
  630. elif re.search(r"一粒|一包|26种|22种|全部|都在", text):
  631. claim = "成分/剂型/便利性"
  632. carrier = "可说成分与剂型事实,不把数字推导成全部所需或补全效果"
  633. else:
  634. claim = "泛营养/生活场景"
  635. carrier = "保留场景和产品事实,减少效果与购买压力"
  636. if account_type.startswith("官方/店铺"):
  637. account_logic = "若属于保健食品且 claim 在批文/广审备案范围内,可作为绿灯表达;超出备案范围仍需弱化或改用隐喻。"
  638. else:
  639. account_logic = "达人/内容账号不承接官方备案能力,claim 需要个人体验化、场景化、隐喻化,并避免强购买引导。"
  640. return {
  641. "claimType": claim,
  642. "claimCarrier": carrier,
  643. "boundaryLogic": f"{sku or '当前SKU'} × {account_type or '未知账号'}:{account_logic}",
  644. }
  645. def match_items(patterns, text):
  646. hits = []
  647. for item in patterns:
  648. key, name, pattern = item if len(item) == 3 else (item[0], item[0], item[1])
  649. if re.search(pattern, text, flags=re.I):
  650. hits.append({"id": key, "name": name})
  651. return hits
  652. def classify_modules(text):
  653. hits = []
  654. for label, pattern in MODULE_PATTERNS:
  655. if re.search(pattern, text, flags=re.I):
  656. hits.append(label)
  657. return hits or ["低信息/泛口播"]
  658. def is_valid_transcript(text):
  659. normalized = normalize_asr(text)
  660. if chinese_count(normalized) < 30:
  661. return False
  662. if re.fullmatch(r"[A-Za-z0-9 ,.'!?-]+", normalized or ""):
  663. return False
  664. return True
  665. def build_rewrite(row):
  666. modules = row["modules"]
  667. suggestions = [REWRITE_TEMPLATES[m] for m in modules if m in REWRITE_TEMPLATES]
  668. if not suggestions:
  669. suggestions = ["保留生活场景和成分信息,删除无法证明的结果承诺,回到标签/说明书口径。"]
  670. return ";".join(dict.fromkeys(suggestions))
  671. RULE_LOOKUP = {item[0]: {"name": item[1], "pattern": item[2]} for item in RULE_PATTERNS}
  672. def rule_name(rule_id):
  673. return FULL_RULE_NAMES.get(rule_id) or RULE_LOOKUP.get(rule_id, {}).get("name", "")
  674. def rule_default_status(rule_id):
  675. try:
  676. num = int(rule_id.replace("RP", ""))
  677. except ValueError:
  678. return "Unknown"
  679. if 1 <= num <= 12:
  680. return "Active-Blocking"
  681. if 13 <= num <= 22:
  682. return "Advisory"
  683. if 23 <= num <= 27:
  684. return "Candidate"
  685. return "Disabled/Park"
  686. def rule_audit_action(rule_id):
  687. status = rule_default_status(rule_id)
  688. if status == "Active-Blocking":
  689. return "红灯截停;删除或改写后再审"
  690. if status == "Advisory":
  691. return "黄灯提示;弱化、补证或人工确认"
  692. if status == "Candidate":
  693. return "候选记录;与其他 Active/Advisory 规则联动判断"
  694. return "不纳入正式判断"
  695. def primary_module_for_rule(row, rule_id):
  696. preferred = RULE_MODULE_MAP.get(rule_id)
  697. modules = row.get("modules") or []
  698. if preferred in modules:
  699. return preferred
  700. for module in modules:
  701. if module != "低信息/泛口播":
  702. return module
  703. return preferred or "低信息/泛口播"
  704. def extract_rule_matches(rule_id, text, max_items=3):
  705. lookup = RULE_LOOKUP.get(rule_id)
  706. if not lookup:
  707. return []
  708. values = []
  709. for match in re.finditer(lookup["pattern"], text or "", flags=re.I):
  710. value = snippet(match.group(0), 48)
  711. if value and value not in values:
  712. values.append(value)
  713. if len(values) >= max_items:
  714. break
  715. return values
  716. def extract_rule_context(rule_id, text, length=180):
  717. lookup = RULE_LOOKUP.get(rule_id)
  718. if not lookup:
  719. return snippet(text, length)
  720. match = re.search(lookup["pattern"], text or "", flags=re.I)
  721. if not match:
  722. return snippet(text, length)
  723. start = max(0, match.start() - 55)
  724. end = min(len(text), match.end() + 115)
  725. return snippet(text[start:end], length)
  726. def build_rule_rewrite(row, rule_id):
  727. module = primary_module_for_rule(row, rule_id)
  728. suggestions = [
  729. RULE_REWRITE_TEMPLATES.get(rule_id),
  730. REWRITE_TEMPLATES.get(module),
  731. ]
  732. return ";".join(dict.fromkeys(item for item in suggestions if item))
  733. def boundary_reusability(row, rule_id):
  734. account_type = row.get("accountType", "")
  735. claim_type = row.get("claimType", "")
  736. if rule_id in {"RP02"}:
  737. return "不可复制高风险"
  738. if account_type.startswith("官方/店铺") and claim_type in {"状态/功效感", "脑力/大脑表现"}:
  739. return "需批文/广审核验后复制"
  740. if account_type.startswith("达人") and rule_id in {"RP18", "RP21"}:
  741. return "达人强导购边界样本"
  742. if rule_id in {"RP01", "RP03", "RP07"}:
  743. return "边界样本,需弱化后复制"
  744. return "可作为经验参考"
  745. def build_success_experience(row, rule_id):
  746. account_type = row.get("accountType", "")
  747. claim_type = row.get("claimType", "")
  748. carrier = row.get("claimCarrier", "")
  749. reusability = boundary_reusability(row, rule_id)
  750. if account_type.startswith("官方/店铺"):
  751. base = "官方/店铺账号的可迁移经验是:把表达落在商品页、包装标签、批文/备案和广告审查可覆盖的信息上;若是保健食品且功能 claim 在备案与广审范围内,可形成绿灯表达。"
  752. elif account_type.startswith("疑似店铺") or account_type.startswith("其他店铺"):
  753. base = "店铺/分销账号的经验需要先核验授权与资质;可复用的是商品事实、版本边界和客服承接,不应默认复制官方功能宣称。"
  754. else:
  755. base = "达人/内容账号的可迁移经验是:个人体验化、生活场景化、隐喻化,不替代官方广告备案,不把内容口播变成强购买引导。"
  756. return f"{base} 本条属于“{claim_type}”,承载方式为“{carrier}”;复用判断:{reusability}。"
  757. def build_golden_cases(valid_rows):
  758. rows_by_rule = defaultdict(list)
  759. for row in valid_rows:
  760. for rule_id in [item for item in row["ruleHits"].split("、") if item]:
  761. rows_by_rule[rule_id].append(row)
  762. golden = []
  763. def sort_rule(rule_id):
  764. return int(rule_id.replace("RP", "")) if rule_id.startswith("RP") else 999
  765. for rule_id in sorted(rows_by_rule, key=sort_rule):
  766. sorted_rows = sorted(
  767. rows_by_rule[rule_id],
  768. key=lambda r: (r["likeCount"], r["commentCount"], r["textLength"]),
  769. reverse=True,
  770. )
  771. for index, row in enumerate(sorted_rows, start=1):
  772. module = primary_module_for_rule(row, rule_id)
  773. name = rule_name(rule_id)
  774. matches = extract_rule_matches(rule_id, row["cleanText"])
  775. comment_examples = [clean(item) for item in row.get("commentExamples", []) if clean(item)]
  776. golden.append({
  777. "id": f"TG-{rule_id}-{index:03d}",
  778. "caseLayer": "逐字稿 Rule 命中案例",
  779. "ruleId": rule_id,
  780. "ruleName": name,
  781. "defaultStatus": rule_default_status(rule_id),
  782. "auditAction": rule_audit_action(rule_id),
  783. "module": module,
  784. "sourceVideoId": row["videoId"],
  785. "sourceTitle": row["title"],
  786. "author": row["author"],
  787. "skuGroup": row["skuGroup"],
  788. "accountType": row.get("accountType", ""),
  789. "accountConfidence": row.get("accountConfidence", ""),
  790. "claimType": row.get("claimType", ""),
  791. "claimCarrier": row.get("claimCarrier", ""),
  792. "boundaryLogic": row.get("boundaryLogic", ""),
  793. "engagement": f"{row['likeCount']}赞/{row['commentCount']}评",
  794. "riskRules": rule_id,
  795. "allRiskRules": row["ruleHits"],
  796. "triggerText": "、".join(matches),
  797. "originalTranscriptSnippet": extract_rule_context(rule_id, row["cleanText"]),
  798. "suggestedApprovedWording": build_rule_rewrite(row, rule_id),
  799. "successExperience": build_success_experience(row, rule_id),
  800. "reusability": boundary_reusability(row, rule_id),
  801. "why": f"命中 {rule_id}({name}),默认状态为 {rule_default_status(rule_id)};该条从真实逐字稿中按规则拆行,用于扩充黄金测试集和审核训练样本。",
  802. "commentEvidence": "\n".join(comment_examples[:3]),
  803. "url": row["url"],
  804. })
  805. return golden
  806. def rule_pack_table_stats():
  807. if not RULE_PACK_DOC.exists():
  808. return {"tableCount": 0, "tableRows": 0}
  809. try:
  810. with zipfile.ZipFile(RULE_PACK_DOC) as docx:
  811. root = ET.fromstring(docx.read("word/document.xml"))
  812. ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}
  813. tables = root.findall(".//w:tbl", ns)
  814. return {
  815. "tableCount": len(tables),
  816. "tableRows": sum(len(table.findall(".//w:tr", ns)) for table in tables),
  817. }
  818. except Exception:
  819. return {"tableCount": 0, "tableRows": 0}
  820. def supplemental_strategy_stats():
  821. if not SUPPLEMENTAL_STRATEGY_MD.exists():
  822. return {"exists": False, "lineCount": 0, "headingCount": 0}
  823. try:
  824. text = SUPPLEMENTAL_STRATEGY_MD.read_text(encoding="utf-8", errors="ignore")
  825. except Exception:
  826. return {"exists": False, "lineCount": 0, "headingCount": 0}
  827. return {
  828. "exists": True,
  829. "lineCount": len(text.splitlines()),
  830. "headingCount": len(re.findall(r"^##+\s+", text, flags=re.M)),
  831. }
  832. def runner_supplement_stats():
  833. if not RUNNER_SUPPLEMENT_JSON.exists():
  834. return {"exists": False, "status": "", "videoCount": 0, "commentCount": 0, "errorCount": 0, "qualityGate": ""}
  835. try:
  836. data = load_json(RUNNER_SUPPLEMENT_JSON)
  837. except Exception:
  838. return {"exists": False, "status": "", "videoCount": 0, "commentCount": 0, "errorCount": 0, "qualityGate": ""}
  839. summary = data.get("summary", {})
  840. quality_gate = data.get("qualityGate", {})
  841. raw_errors = []
  842. if RUNNER_SUPPLEMENT_RAW.exists():
  843. try:
  844. raw_errors = load_json(RUNNER_SUPPLEMENT_RAW).get("metadata", {}).get("errors", [])
  845. except Exception:
  846. raw_errors = []
  847. return {
  848. "exists": True,
  849. "status": data.get("status", ""),
  850. "videoCount": int(summary.get("videoCount") or 0),
  851. "commentCount": int(summary.get("commentCount") or 0),
  852. "errorCount": len(raw_errors) or int(summary.get("errorCount") or 0),
  853. "qualityGate": quality_gate.get("level", ""),
  854. }
  855. def classify_public_title_risk(title):
  856. hits = match_items(PUBLIC_TITLE_RISK_PATTERNS, title)
  857. if not hits:
  858. return {
  859. "riskText": "未命中高频标题风险;仍需逐字稿/画面复核",
  860. "riskRules": "",
  861. "reuse": "标题观察样本,待逐字稿确认",
  862. }
  863. rules = "、".join(item["id"] for item in hits)
  864. names = ";".join(f"{item['id']} {item['name']}" for item in hits)
  865. high_risk_rules = {item["id"] for item in hits}
  866. if "RP18" in high_risk_rules or "RP21" in high_risk_rules:
  867. reuse = "促销/CTA 边界样本;活动可承接,但不要绑定功效承诺"
  868. elif {"RP01", "RP03", "RP13"} & high_risk_rules:
  869. reuse = "标题边界样本;需弱化为日常营养补充/标签事实后再复制"
  870. else:
  871. reuse = "标题观察样本,需结合口播、字幕、商品卡确认"
  872. return {
  873. "riskText": names,
  874. "riskRules": rules,
  875. "reuse": reuse,
  876. }
  877. def load_public_profile_samples(official_names):
  878. def load_list(path):
  879. if not path.exists():
  880. return []
  881. try:
  882. data = load_json(path)
  883. except Exception:
  884. return []
  885. return data if isinstance(data, list) else []
  886. all_samples = load_list(PUBLIC_PROFILE_ALL_VIDEOS)
  887. new_samples = load_list(PUBLIC_PROFILE_NEW_VIDEOS)
  888. seen = set()
  889. rows = []
  890. for sample in new_samples:
  891. video_id = clean(sample.get("videoId"))
  892. if not video_id or video_id in seen:
  893. continue
  894. seen.add(video_id)
  895. title = clean(sample.get("title"))
  896. author = clean(sample.get("author"))
  897. row = {
  898. "videoId": video_id,
  899. "author": author,
  900. "title": title,
  901. "url": clean(sample.get("url")),
  902. "profileUrl": clean(sample.get("profileUrl")),
  903. "source": clean(sample.get("source")) or "public_profile_playwright",
  904. "collectedAt": clean(sample.get("collectedAt")),
  905. }
  906. row.update(classify_account(author, official_names))
  907. row.update(claim_boundary_for_row(row))
  908. risk = classify_public_title_risk(title)
  909. row.update(risk)
  910. row["evidenceStatus"] = "公开视频主页标题/账号可见证据;暂无评论、媒体下载和逐字稿,不并入逐字稿黄金案例。"
  911. rows.append(row)
  912. account_counter = Counter(row.get("accountType", "") for row in rows)
  913. return {
  914. "exists": PUBLIC_PROFILE_NEW_VIDEOS.exists(),
  915. "allCount": len({clean(item.get("videoId")) for item in all_samples if clean(item.get("videoId"))}) or len(rows),
  916. "newCount": len(rows),
  917. "rows": rows,
  918. "accountCounts": [{"accountType": k, "videoCount": v} for k, v in account_counter.most_common()],
  919. }
  920. def main():
  921. live = load_json(LIVE_DATA)
  922. account_list = load_account_list()
  923. official_names = account_list["names"]
  924. video_rows = []
  925. for row in live["videoRows"]:
  926. item = dict(row)
  927. item.update(classify_account(item.get("author", ""), official_names))
  928. item.update(claim_boundary_for_row(item))
  929. item["accountBoundaryNote"] = account_rule_notes(item["accountType"])
  930. video_rows.append(item)
  931. videos_by_id = {str(row["videoId"]): row for row in video_rows}
  932. comments_by_video = defaultdict(list)
  933. for row in live["commentRows"]:
  934. comments_by_video[str(row.get("videoId"))].append(row)
  935. transcript_rows = []
  936. for transcript_path in sorted((TRANSCRIPT_DIR / "transcripts").glob("*/transcript.json")):
  937. data = load_json(transcript_path)
  938. video_id = str(data.get("awemeId") or transcript_path.parent.name)
  939. video = videos_by_id.get(video_id, {})
  940. raw_text = clean(data.get("text"))
  941. text = normalize_asr(raw_text)
  942. valid = data.get("status") == "ok" and is_valid_transcript(text)
  943. modules = classify_modules(text) if valid else ["无效/低信息逐字稿"]
  944. rules = match_items(RULE_PATTERNS, text) if valid else []
  945. row_obj = {
  946. "videoId": video_id,
  947. "title": video.get("title", ""),
  948. "author": video.get("author", ""),
  949. "keyword": video.get("keyword", ""),
  950. "skuGroup": video.get("skuGroup", ""),
  951. "riskLevel": video.get("riskLevel", ""),
  952. "likeCount": video.get("likeCount", 0),
  953. "commentCount": video.get("commentCount", 0),
  954. "url": video.get("url", ""),
  955. "transcriptStatus": data.get("status", ""),
  956. "durationMs": data.get("durationMs", 0),
  957. "rawText": raw_text,
  958. "cleanText": text,
  959. "textLength": len(text),
  960. "chineseCount": chinese_count(text),
  961. "validTranscript": valid,
  962. "modules": modules,
  963. "moduleText": "、".join(modules),
  964. "ruleHits": "、".join(item["id"] for item in rules),
  965. "ruleNames": "、".join(item["name"] for item in rules),
  966. "rewriteDirection": "",
  967. "commentExamples": [c.get("text", "") for c in comments_by_video.get(video_id, [])[:3]],
  968. "accountType": video.get("accountType", ""),
  969. "accountConfidence": video.get("accountConfidence", ""),
  970. "accountRuleLogic": video.get("accountRuleLogic", ""),
  971. }
  972. row_obj.update(claim_boundary_for_row(row_obj))
  973. row_obj["accountBoundaryNote"] = account_rule_notes(row_obj["accountType"])
  974. transcript_rows.append(row_obj)
  975. for row in transcript_rows:
  976. row["rewriteDirection"] = build_rewrite(row) if row["validTranscript"] else "不纳入话术结论;仅记录为转写低信息/疑似背景音乐。"
  977. valid_rows = [row for row in transcript_rows if row["validTranscript"]]
  978. invalid_rows = [row for row in transcript_rows if not row["validTranscript"]]
  979. module_counter = Counter()
  980. rule_counter = Counter()
  981. account_counter = Counter(row.get("accountType", "") for row in video_rows)
  982. valid_account_counter = Counter(row.get("accountType", "") for row in valid_rows)
  983. for row in valid_rows:
  984. module_counter.update(row["modules"])
  985. rule_counter.update([item for item in row["ruleHits"].split("、") if item])
  986. golden = build_golden_cases(valid_rows)
  987. run_log = load_json(RUN_LOG) if RUN_LOG.exists() else []
  988. run_log_video_ids = {str(row.get("videoId", "")) for row in run_log if row.get("videoId")}
  989. transcript_video_ids = {str(row.get("videoId", "")) for row in transcript_rows if row.get("videoId")}
  990. attempted_video_ids = run_log_video_ids | transcript_video_ids
  991. failed_run_log_ids = {
  992. str(row.get("videoId", ""))
  993. for row in run_log
  994. if row.get("videoId") and row.get("transcriptStatus") != "ok"
  995. } - transcript_video_ids
  996. rule_pack_stats = rule_pack_table_stats()
  997. supplemental_stats = supplemental_strategy_stats()
  998. runner_stats = runner_supplement_stats()
  999. public_profile_stats = load_public_profile_samples(official_names)
  1000. summary = {
  1001. "generatedAt": datetime.now().astimezone().isoformat(),
  1002. "liveVideoCount": live["summary"]["videoCount"],
  1003. "liveCommentCount": live["summary"]["commentCount"],
  1004. "publicProfileVideoCount": public_profile_stats["allCount"],
  1005. "publicProfileNewVideoCount": public_profile_stats["newCount"],
  1006. "transcriptAttemptSelectedCount": len(attempted_video_ids),
  1007. "transcriptFileCount": len(transcript_rows),
  1008. "validTranscriptCount": len(valid_rows),
  1009. "invalidTranscriptCount": len(invalid_rows),
  1010. "runLogOkCount": sum(1 for row in transcript_rows if row.get("transcriptStatus") == "ok"),
  1011. "runLogFailedCount": len(failed_run_log_ids),
  1012. "goldenTranscriptCount": len(golden),
  1013. "rulePackTableCount": rule_pack_stats["tableCount"],
  1014. "rulePackTableRows": rule_pack_stats["tableRows"],
  1015. "supplementalStrategyDocExists": supplemental_stats["exists"],
  1016. "supplementalStrategyDocLines": supplemental_stats["lineCount"],
  1017. "supplementalStrategyDocHeadings": supplemental_stats["headingCount"],
  1018. "runnerSupplementExists": runner_stats["exists"],
  1019. "runnerSupplementStatus": runner_stats["status"],
  1020. "runnerSupplementVideoCount": runner_stats["videoCount"],
  1021. "runnerSupplementCommentCount": runner_stats["commentCount"],
  1022. "runnerSupplementErrorCount": runner_stats["errorCount"],
  1023. "runnerSupplementQualityGate": runner_stats["qualityGate"],
  1024. "officialAccountListCount": len(account_list["records"]),
  1025. "accountCounts": [{"accountType": k, "videoCount": v} for k, v in account_counter.most_common()],
  1026. "validTranscriptAccountCounts": [{"accountType": k, "validTranscriptCount": v} for k, v in valid_account_counter.most_common()],
  1027. "publicProfileAccountCounts": public_profile_stats["accountCounts"],
  1028. "moduleCounts": [{"module": k, "count": v} for k, v in module_counter.most_common()],
  1029. "ruleCounts": [{"rule": k, "count": v} for k, v in rule_counter.most_common()],
  1030. }
  1031. report = {
  1032. "summary": summary,
  1033. "transcriptRows": transcript_rows,
  1034. "moduleSummary": summary["moduleCounts"],
  1035. "ruleSummary": summary["ruleCounts"],
  1036. "goldenTranscriptRows": golden,
  1037. "invalidTranscriptRows": invalid_rows,
  1038. "liveSummary": live["summary"],
  1039. "commentThemeSummary": live["themeSummary"],
  1040. "videoRows": video_rows,
  1041. "commentRows": live["commentRows"],
  1042. "publicProfileTitleRows": public_profile_stats["rows"],
  1043. "officialAccountRecords": account_list["records"],
  1044. }
  1045. OUT_JSON.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
  1046. OUT_MD.write_text(build_markdown(report), encoding="utf-8")
  1047. print(json.dumps({"status": "ok", "output": str(OUT_JSON), "markdown": str(OUT_MD), **summary}, ensure_ascii=False, indent=2))
  1048. def build_markdown(report):
  1049. s = report["summary"]
  1050. golden_rows = report["goldenTranscriptRows"]
  1051. public_profile_rows = report.get("publicProfileTitleRows", [])
  1052. golden_by_rule = defaultdict(list)
  1053. for row in golden_rows:
  1054. golden_by_rule[row.get("ruleId", row.get("riskRules", ""))].append(row)
  1055. observed_rule_ids = [row["rule"] for row in report["ruleSummary"]]
  1056. observed_rule_set = set(observed_rule_ids)
  1057. all_rule_ids = [f"RP{index:02d}" for index in range(1, 29)]
  1058. unobserved_rule_ids = [rule_id for rule_id in all_rule_ids if rule_id not in observed_rule_set]
  1059. account_counts = s.get("accountCounts", [])
  1060. valid_account_counts = s.get("validTranscriptAccountCounts", [])
  1061. account_video_map = {row.get("accountType", ""): row.get("videoCount", 0) for row in account_counts}
  1062. official_video_count = sum(count for account_type, count in account_video_map.items() if account_type.startswith("官方/店铺"))
  1063. creator_video_count = sum(count for account_type, count in account_video_map.items() if account_type.startswith("达人/内容"))
  1064. other_store_video_count = sum(
  1065. count
  1066. for account_type, count in account_video_map.items()
  1067. if account_type.startswith("其他店铺") or account_type.startswith("疑似店铺")
  1068. )
  1069. lines = [
  1070. "# 善存抖音逐字稿话术审核分析",
  1071. "",
  1072. f"生成时间:{s['generatedAt']}",
  1073. "",
  1074. "## 样本与方法",
  1075. "",
  1076. f"- 抖音网关实采去重视频:{s['liveVideoCount']} 条;评论:{s['liveCommentCount']} 条。",
  1077. f"- 公开主页补采:Playwright 已从官方/店铺公开主页抽取 {s.get('publicProfileVideoCount', 0)} 条公开视频列表,其中与既有网关样本去重后新增 {s.get('publicProfileNewVideoCount', 0)} 条标题样本;该批为标题/账号可见证据,未计入评论和逐字稿数量。",
  1078. f"- 本轮优先转写高互动/高风险/营养包及多 SKU 样本,已生成逐字稿文件:{s['transcriptFileCount']} 条,其中有效口播逐字稿:{s['validTranscriptCount']} 条,低信息/疑似背景音乐:{s['invalidTranscriptCount']} 条。",
  1079. f"- 参考资料文档包含 {s.get('rulePackTableCount', 0)} 个表格、{s.get('rulePackTableRows', 0)} 行表格样例;本轮黄金边界数据集已从摘要式 12 行扩充为 {s.get('goldenTranscriptCount', 0)} 行,按真实逐字稿的 Rule Pack 命中逐条拆分。",
  1080. f"- 补充参考稿 `抖音保健品过审黄金公式与边界策略.md` 已纳入结构对齐:已读取,行数={s.get('supplementalStrategyDocLines', 0)},标题数={s.get('supplementalStrategyDocHeadings', 0)}。",
  1081. f"- 本轮补跑说明:没有在网关权限失败后停止;已继续尝试关键词、官方账号、已知 sec_user_id、单视频详情、搜索页和公开主页。实时网关仍显示权限不足或搜索失败(runner 状态={s.get('runnerSupplementStatus', '') or '未生成'},错误数={s.get('runnerSupplementErrorCount', 0)}),但公开主页路径新增 {s.get('publicProfileNewVideoCount', 0)} 条官方/店铺标题样本;新增有效逐字稿仍为 0,逐字稿结论仍以现有 {s['transcriptFileCount']} 个逐字稿文件为证据。",
  1082. "- 媒体链路:从抖音搜索原始返回恢复视频播放地址,使用 ffmpeg/媒体直传处理后调用 iflytek-gateway 转写;逐字稿用于话术级审核分析。",
  1083. "- 限制说明:ASR 存在同音误识别,例如“营养包”可能识别成“阴阳包”;本报告已做轻量清洗,但关键结论仍建议结合视频画面和品牌原稿复核。",
  1084. "",
  1085. "## 采集执行审计(回应“是否按要求采集”)",
  1086. "",
  1087. "结论:本轮有按要求做抖音采集、补采、转写和评论复核,不是只基于资料文档脑补。当前可交付证据来自抖音网关已实采样本;追加扩量已执行多条路径,但实时网关权限/搜索链路未返回新增有效视频,所以本版把“已执行动作”和“新增数据受阻”分开呈现,避免把采集失败误写成未采集。",
  1088. "",
  1089. "| 采集/分析要求 | 已执行动作 | 当前证据 | 审视判断 | 后续补齐条件 |",
  1090. "|---|---|---|---|---|",
  1091. f"| 善存主品牌、营养包及其他 SKU | 以善存、善存 PRO/每日营养包、多维、男士/女士/50+、海外/跨境、小佳维等作为样本池入口。 | 网关实采去重视频 {s['liveVideoCount']} 条、评论 {s['liveCommentCount']} 条。 | 已形成本版证据底座。 | 后续继续补银善存/大脑、官方店铺专题和特殊人群专题。 |",
  1092. f"| 官方/店铺账号与达人账号分层 | 读取 `店铺与账号.xlsx`,将视频作者按官方/店铺、达人/内容、其他店铺/分销拆分。 | 官方/店铺清单 {s.get('officialAccountListCount', 0)} 条;样本中官方/店铺 {official_video_count} 条、达人/内容 {creator_video_count} 条、其他店铺/分销 {other_store_video_count} 条。 | 已满足老板强调的账号身份区分。 | 仍需后台蓝V认证、授权关系和具体产品广审资料确认。 |",
  1093. f"| 高风险话术逐字稿分析 | 优先选择高互动、高风险、营养包、多 SKU 和边界 claim 样本做 ASR 转写。 | 已选 {s.get('transcriptAttemptSelectedCount', 0)} 条;生成逐字稿 {s['transcriptFileCount']} 条,有效口播 {s['validTranscriptCount']} 条,低信息 {s['invalidTranscriptCount']} 条。 | 已从标题/标签推进到真实口播。 | 8 条旧样本媒体回查未找到,需新链接或可访问媒体源。 |",
  1094. f"| 黄金数据集与样本案例 | 按 Rule Pack 命中将有效逐字稿拆成案例行,而不是抽少量展示。 | 黄金边界案例 {s.get('goldenTranscriptCount', 0)} 行,覆盖当前有效逐字稿命中的 {len(observed_rule_ids)}/28 个 RP。 | 已满足“黄金数据集和样本案例”的 MD 版本要求。 | Excel 等 MD 口径确认后再按同字段重建。 |",
  1095. f"| 评论与改进空间 | 对已投放视频评论做主题复核,回看口播诱因和评论区承接。 | 评论 {s['liveCommentCount']} 条;已输出评论主题、典型评论、口播诱因、客服/置顶回复建议。 | 已覆盖用户反馈和改进空间。 | 下一轮可按高赞、负面体验、功效追问做更深分层。 |",
  1096. f"| 定向补采:官方、银善存/大脑、强 CTA、特殊人群 | 已尝试项目采集脚本、通用 runner、已知账号/关键词、单视频详情、搜索页和公开主页路径。 | runner 状态 `{s.get('runnerSupplementStatus', '') or '未生成'}`,错误数 {s.get('runnerSupplementErrorCount', 0)};公开主页新增标题样本 {s.get('publicProfileNewVideoCount', 0)} 条;新增有效逐字稿 0 条。 | 已执行但受权限/搜索链路阻塞,不能写成全网穷尽覆盖。 | 恢复抖音网关权限,或由客户提供可访问视频链接/文件。 |",
  1097. "",
  1098. "对老板的可交付判断:方向和结构已经按要求来,证据也用足了当前可访问数据;剩余缺口主要不是 MD 结构,而是新增抖音实时数据权限、银善存/大脑专题样本量,以及客户侧产品批文/蓝帽/广审/商品页资料。",
  1099. "",
  1100. "## 逐字稿结论",
  1101. "",
  1102. "本轮口径已调整:抖音采集到的是“已经发出来的成功案例”,不是待审核稿件。因此分析重点不是告诉业务“这条怎么改更好过审”,而是反推这些已发布稿件为什么能踩在边界上发出来:它用了什么账号身份、产品资质、卖点承载、隐喻画面、人群标签、弱化措辞或商品页承接,形成了怎样的成功经验。",
  1103. "",
  1104. "真实口播比标题/标签暴露出更强的边界动作:不少视频不只在标题里出现“精力满格、气血满格、代谢、甲流、一粒补充”,口播中还会进一步强化“全部都调配好了、每天一粒/一包就够、身体底子/皮肤状态慢慢养稳、明星都在吃、直播间快冲”等表达。这里要区分两类结论:一类是可迁移经验,例如“成分事实 + 标签建议 + 官方/客服承接”;另一类是撞审/边界样本,例如达人强 CTA、疾病词、未备案范围功效隐喻,只能作为风险边界观察,不能直接复制。",
  1105. "",
  1106. "本轮补齐后,MD 不再把黄金边界数据集压缩成少量展示样本,而是把每条有效逐字稿按 Rule Pack 命中拆成成功边界案例。这样做的好处是:同一视频里“每天一粒就够”“26 种营养”“直播间快冲”“30+女性”“海外版/正品”等风险点会各自成行,方便回流到规则测试、达人 brief、审核标注和后续 Excel 明细。",
  1107. "",
  1108. "## 目标口径校正",
  1109. "",
  1110. "| 旧口径 | 新口径 | 报告处理方式 |",
  1111. "|---|---|---|",
  1112. "| 怎么改这条稿件更好过审 | 为什么这条已发布稿件能发出来 | 每个黄金案例增加“账号类型、claim 类型、承载方式、边界逻辑、复用判断”。 |",
  1113. "| 把红灯都写成删改建议 | 区分可复制经验和撞审边界 | 官方/店铺账号若有保健食品批文与广审备案,部分功能 claim 可绿灯;达人账号不能借官方备案直接说功能或强导购。 |",
  1114. "| 按规则命中给风险 | 按账号×产品×卖点×人群×claim 组合找边界 | 增加策略公式清单,帮助后续批量跑样本时归纳成功路径。 |",
  1115. "| 不区分账号身份 | 明确蓝V/官方店铺、疑似店铺、达人/内容账号不同边界 | 使用 `店铺与账号.xlsx` 做官方/店铺账号清单匹配;未在清单但名称像店铺的账号标为待核验。 |",
  1116. "",
  1117. "## 老板要求逐条对照结论",
  1118. "",
  1119. "结论:当前 MD 已按老板补充方向来做,核心定位是“从抖音已发布成功案例反推审核边界和可复制经验”,不是单纯写敏感词删改。需要对外说明的是:当前样本是已采集可用数据,不等于全网最终覆盖;新增补采受抖音网关权限/搜索失败影响,后续恢复权限后可继续扩容。上方“采集执行审计”已单独回答是否按要求采集。",
  1120. "",
  1121. "| 老板/客户要求 | 当前文档落点 | 完成度 | 还需要补什么 |",
  1122. "|---|---|---|---|",
  1123. f"| 目标品牌善存,覆盖营养包及其他 SKU | 样本覆盖善存 PRO/每日营养包、复合维生素/多维、男士/女士/50+、海外/跨境版本、小佳维等;总计 {s['liveVideoCount']} 条去重视频。 | 已覆盖当前可用样本 | 后续按缺口补采银善存/大脑、官方店铺、特殊人群和强 CTA 专题。 |",
  1124. f"| 抖音渠道已发布稿件如何通过审核 | `逐字稿结论`、`目标口径校正`、`过审边界策略公式`、`Rule Pack 逐条复盘`均按“已发布成功案例反推边界”组织。 | 已落实 | 需要品牌提供原始待审稿/修改前稿时,才能进一步做“修改前后差异”。 |",
  1125. f"| 需要黄金数据集和样本案例 | 已把有效逐字稿按 Rule Pack 命中拆成 {s.get('goldenTranscriptCount', 0)} 行黄金边界案例,并保留重点逐字稿样本、视频 ID、触发词、账号类型、claim 类型和复用判断。 | 已落实 | Excel 待 MD 方向确认后再补齐筛选字段。 |",
  1126. f"| 是否要转逐字稿并分析话术 | 已生成逐字稿 {s['transcriptFileCount']} 条,其中有效口播 {s['validTranscriptCount']} 条;分析从标题/标签推进到真实口播。 | 已落实 | ASR 同音误识别处建议结合视频画面和品牌原稿复核。 |",
  1127. f"| 已投放稿件用户评论如何,有无改进空间 | 已基于 {s['liveCommentCount']} 条评论做主题复核,拆出泛互动、正品/渠道、购买路径、达人明星、低质互动、体验/副作用等,并给置顶评论/客服回复建议。 | 已落实 | 可在下一轮按“高赞评论、负面体验、功效追问”继续深挖。 |",
  1128. f"| 结合资料、规则和客户理想稿 | 已读取 Rule Pack 文档 {s.get('rulePackTableCount', 0)} 个表格/{s.get('rulePackTableRows', 0)} 行样例、补充参考稿 {s.get('supplementalStrategyDocLines', 0)} 行/{s.get('supplementalStrategyDocHeadings', 0)} 个标题,并接入官方/店铺账号清单 {len(report.get('officialAccountRecords', []))} 条。 | 已落实 | 仍需客户补产品批文、蓝帽/备案范围、广审批件、商品页标签资料。 |",
  1129. f"| 尽可能多的视频样本 | 当前已有 {s['liveVideoCount']} 条视频、{s['liveCommentCount']} 条评论;已做关键词、账号、单视频详情、搜索页、公开主页等补采尝试。 | 已执行,新增受阻 | 本轮补采因 runner 状态 `{s.get('runnerSupplementStatus', '') or '未生成'}`、错误数 {s.get('runnerSupplementErrorCount', 0)} 未新增有效样本,需恢复抖音网关权限或提供视频链接。 |",
  1130. "| 先补 MD,Excel 后确认 | 本轮只优先补 MD,并把后续 Excel 字段放进复用判断和下一步建议。 | 已按用户要求执行 | 等你确认 MD 口径后再生成/补齐 Excel。 |",
  1131. "",
  1132. "## 补充需求对齐:客户理想稿结构",
  1133. "",
  1134. "新增参考稿的核心不是单一品牌复盘,而是要沉淀一套“合规审核 + 脚本修改建议”的技能框架。本版将善存报告从样本复盘升级为三层结构:先给法规和账号/产品/claim 的通用判断框架,再给善存已发布案例的边界证据,最后给可系统化实现的功能模块。",
  1135. "",
  1136. "| 客户理想稿模块 | 本版善存报告承接方式 |",
  1137. "|---|---|",
  1138. "| 前置合规基础 | 增加强制提示语、产品身份区分、账号资质要求,明确不能把跨境/普通食品/蓝帽保健食品混用。 |",
  1139. "| 蓝V vs 达人双轨制 | 使用官方账号清单区分官方/店铺、疑似店铺、达人/内容账号,给出不同 claim 上限。 |",
  1140. "| 功效宣称红绿灯 | 将 claim 拆成 Level 0 到 Level 5,分别判断生活场景、食用体验、成分事实、备案内功能、备案外功能和疾病治疗。 |",
  1141. "| 撞审边界技巧 | 将概念替换、比喻暗示、成分事实、标签回流、身份脱敏、短脚本冲量等提炼为技巧库。 |",
  1142. "| 系统功能需求 | 追加“合规审核与脚本建议系统功能模块”,方便后续做成技能、工作流或产品模块。 |",
  1143. "",
  1144. "### 话语体系对照(含法规依据)",
  1145. "",
  1146. "参考稿的标准不是简单列敏感词,而是把“高危话术 -> 安全话术 -> 法规/平台依据 -> 善存样本落点”做成可复用替换库。以下按善存营养包、多维、海外版和达人/官方账号的真实样本改写。",
  1147. "",
  1148. "| 维度 | 高危话术 | 安全话术 | 法规/规则依据 | 善存样本处理 |",
  1149. "|---|---|---|---|---|",
  1150. ]
  1151. for row in SPEECH_SYSTEM_ROWS:
  1152. lines.append(f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} | {row[4]} |")
  1153. lines.extend([
  1154. "",
  1155. "### 前置合规基础",
  1156. "",
  1157. "保健食品广告/推广内容需显著标明:**保健食品不是药物,不能代替药物治疗疾病**。这条不是“降低风险的建议”,而是保健食品广告内容里的硬约束;但它也不是万能护身符,达人账号即使加提示语,也不能因此获得保健食品广告发布或代言推荐资格。实际发布仍需以品牌法务、RA、广审批件、商品页和平台规则确认。",
  1158. "",
  1159. "| 产品类型 | 识别依据 | claim 上限 | 善存报告处理方式 |",
  1160. "|---|---|---|---|",
  1161. ])
  1162. for row in PRODUCT_TYPE_BOUNDARY_ROWS:
  1163. lines.append(f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} |")
  1164. lines.extend([
  1165. "",
  1166. "### 账号类型双轨制",
  1167. "",
  1168. "| 账号类型 | 资质/身份前提 | 可承接内容 | 不可承接内容 |",
  1169. "|---|---|---|---|",
  1170. ])
  1171. for row in ACCOUNT_TRACK_ROWS:
  1172. lines.append(f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} |")
  1173. lines.extend([
  1174. "",
  1175. "### 功效宣称五级层次",
  1176. "",
  1177. "| 层级 | 类型 | 示例 | 默认判断 | 关键边界 |",
  1178. "|---|---|---|---|---|",
  1179. ])
  1180. for row in CLAIM_LEVEL_ROWS:
  1181. lines.append(f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} | {row[4]} |")
  1182. lines.extend([
  1183. "",
  1184. "善存现有逐字稿里最常见的是 Level 1 成分事实、Level 0/0.5 生活体验和 Level 4 边界功效感混在一起。例如“26 种营养”本身是 Level 1,但一旦接成“每天一粒就够、全部补齐、状态变好”,就上升到 Level 4 风险;“明星都在吃”若变成适用性或效果证明,则进入达人/代言红线。",
  1185. "",
  1186. "### 核心判断矩阵(含账号×渠道双维度)",
  1187. "",
  1188. "同一句话不能只看词,还要同时看账号身份、产品属性、claim 层级和发布承接。以下矩阵用于把已发布善存样本拆成“可复制、需核验、边界样本、不可复制”。",
  1189. "",
  1190. "| 宣称类型 | 默认状态 | 官方/店铺账号 | 达人/内容账号 | 前提条件 | 本报告复用结论 |",
  1191. "|---|---|---|---|---|---|",
  1192. ])
  1193. for row in CORE_JUDGMENT_MATRIX_ROWS:
  1194. lines.append(f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} | {row[4]} | {row[5]} |")
  1195. lines.extend([
  1196. "",
  1197. "### 10 大技巧中的善存适配版",
  1198. "",
  1199. "| 技巧 | 公式 | 善存适配示例 | 风险 | 样本观察 |",
  1200. "|---|---|---|---|---|",
  1201. ])
  1202. for row in BOUNDARY_TECHNIQUE_ROWS:
  1203. lines.append(f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} | {row[4]} |")
  1204. lines.extend([
  1205. "",
  1206. "### 合规审核与脚本建议系统功能模块",
  1207. "",
  1208. "| 模块 | 输入/动作 | 输出价值 |",
  1209. "|---|---|---|",
  1210. ])
  1211. for row in SYSTEM_FUNCTION_ROWS:
  1212. lines.append(f"| {row[0]} | {row[1]} | {row[2]} |")
  1213. lines.extend([
  1214. "",
  1215. "## 三个核心合规原则(确保所有策略落地)",
  1216. "",
  1217. "参考稿里最值得保留的是三个原则:不是为了写得像法规摘要,而是为了让脚本、评论、商品卡、达人 brief 和审核系统都能共用同一个判断顺序。",
  1218. "",
  1219. ])
  1220. for principle in COMPLIANCE_PRINCIPLE_ROWS:
  1221. lines.extend([
  1222. f"### {principle['title']}",
  1223. "",
  1224. f"- 原则说明:{principle['content']}",
  1225. f"- 发布前核验:{principle['check']}",
  1226. f"- 善存当前证据:{principle['evidence']}",
  1227. "",
  1228. ])
  1229. lines.extend([
  1230. "## 规则依据底座",
  1231. "",
  1232. "以下规则只作为边界判断底座,不能替代品牌法务、产品注册/备案资料、广告审查批件和平台最新规则。尤其是“功能宣称”:一般情况下属于红灯高风险;但如果产品是保健食品,且该功能在注册证书/备案凭证、说明书和广审备案范围内,并由官方/店铺账号按审查内容一致发布,则可以形成绿灯表达。",
  1233. "",
  1234. "| 依据 | 用在本报告中的作用 | 链接 |",
  1235. "|---|---|---|",
  1236. ])
  1237. for ref in LEGAL_REFERENCES:
  1238. lines.append(f"| {ref['name']} | {ref['use']} | {ref['url']} |")
  1239. lines.extend([
  1240. "",
  1241. "## 账号身份分层",
  1242. "",
  1243. f"本轮读取 `店铺与账号.xlsx` 后,识别到 {s.get('officialAccountListCount', 0)} 条官方/店铺账号记录。报告将清单命中的作者暂按“官方/店铺账号(清单匹配)”处理;是否蓝V、是否具备对应广告备案能力,仍需以账号后台认证和产品广审材料复核。",
  1244. "",
  1245. "| 账号类型 | 视频样本数 | 有效逐字稿数 | 可说边界 | 不宜复制边界 |",
  1246. "|---|---:|---:|---|---|",
  1247. ])
  1248. valid_account_map = {row["accountType"]: row.get("validTranscriptCount", 0) for row in valid_account_counts}
  1249. for row in account_counts:
  1250. account_type = row["accountType"]
  1251. if account_type.startswith("官方/店铺"):
  1252. allow = "可承接商品资质、标签说明、批文/备案范围内的功能 claim、活动规则和客服说明。"
  1253. avoid = "不能超出备案/广审范围;不能口播字幕画面与商品页不一致。"
  1254. elif account_type.startswith("疑似店铺") or account_type.startswith("其他店铺"):
  1255. allow = "可观察为店铺型边界样本,但要先核验授权、账号认证、商品资质和广告审查范围。"
  1256. avoid = "不能默认套用官方账号能力;不把未核验功能宣称写成绿灯经验。"
  1257. else:
  1258. allow = "可说个人体验、生活场景、成分事实、中性科普和非强导购互动。"
  1259. avoid = "不能借官方备案说功能;不宜强 CTA、价格催单、治疗/预防、保证效果。"
  1260. lines.append(f"| {account_type} | {row['videoCount']} | {valid_account_map.get(account_type, 0)} | {allow} | {avoid} |")
  1261. lines.extend([
  1262. "",
  1263. "## 过审边界策略公式",
  1264. "",
  1265. "可把后续样本按以下公式打标签:`账号身份 × 产品属性 × 卖点类型 × 人群标签 × 功效 claim × 承载方式 × 购买引导强度 = 发布边界`。",
  1266. "",
  1267. "### 账号类型 × 宣称层次 → 过审策略",
  1268. "",
  1269. "| 账号类型 | 宣称层次 | 过审策略 | 执行动作 |",
  1270. "|---|---|---|---|",
  1271. ])
  1272. for row in ACCOUNT_CLAIM_STRATEGY_ROWS:
  1273. lines.append(f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} |")
  1274. lines.extend([
  1275. "",
  1276. "### 产品属性 × 目标人群 → 过审策略",
  1277. "",
  1278. "| 产品属性 | 目标人群 | 可承接卖点 | 高危延伸 | 过审策略 |",
  1279. "|---|---|---|---|---|",
  1280. ])
  1281. for row in PRODUCT_PERSON_STRATEGY_ROWS:
  1282. lines.append(f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} | {row[4]} |")
  1283. lines.extend([
  1284. "",
  1285. "### 卖点维度 × 话术替换公式(含法规依据)",
  1286. "",
  1287. "| 卖点维度 | 高危话术 | 替换公式 | 规则依据 | 适用账号 |",
  1288. "|---|---|---|---|---|",
  1289. ])
  1290. for row in SELLING_POINT_REWRITE_ROWS:
  1291. lines.append(f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} | {row[4]} |")
  1292. lines.extend([
  1293. "",
  1294. "### 善存已发布策略公式库",
  1295. "",
  1296. "| 公式 | 适用条件 | 已发布成功经验 | 复用判断 |",
  1297. "|---|---|---|---|",
  1298. ])
  1299. for row in STRATEGY_FORMULA_ROWS:
  1300. lines.append(f"| {row['formula']} | {row['condition']} | {row['experience']} | {row['reuse']} |")
  1301. lines.extend([
  1302. "",
  1303. "公式使用说明:同一个视频可以命中多个公式,例如“官方店铺账号 + 海外版 + 26 种成分 + 直播活动”应拆成成分事实、跨境版本、价格活动三条边界分别判断;达人账号出现同样内容时,不能自动继承官方/店铺账号的功能宣称能力。",
  1304. "",
  1305. "对老板关心的“撞审/边界通过”,本报告不把它写成稳定绿灯,而是用复用判断分为四类:`高可复制`、`需资料核验后复制`、`边界样本需逐条审核`、`不可复制`。后续 Excel 可把这四类做成筛选字段。",
  1306. "",
  1307. "## AI可用的过审策略公式清单",
  1308. "",
  1309. "这一节把前面的人工判断压缩成可编码规则。后续做成系统时,可以先用规则引擎打底,再让模型只负责解释、归类和生成改写建议,避免模型自由发挥法规结论。",
  1310. "",
  1311. ])
  1312. for group in AI_FORMULA_GROUPS:
  1313. lines.extend([
  1314. f"### {group['title']}",
  1315. "",
  1316. "| 规则 |",
  1317. "|---|",
  1318. ])
  1319. for item in group["items"]:
  1320. lines.append(f"| {item} |")
  1321. lines.append("")
  1322. lines.extend([
  1323. "## 与资料文档对齐",
  1324. "",
  1325. f"- 资料文件 `QW1_Rule_Pack_ExamplesV0.1.docx` 当前识别到 {s.get('rulePackTableCount', 0)} 个表格、{s.get('rulePackTableRows', 0)} 行表格内容。该文档的结构是“Rule Pack 总览 + 每个 RP 的红灯反例/黄灯样例/绿灯参考/识别线索/推荐改法”。",
  1326. f"- 当前抖音逐字稿样本命中 {len(observed_rule_ids)}/28 个 RP,扩充后的黄金边界数据集为 {s.get('goldenTranscriptCount', 0)} 行。它不是复制资料文档的规则行,而是把善存抖音真实口播补成可训练案例。",
  1327. "- 行数仍低于资料文档总行数,是因为本轮只基于已采集且有效转写的善存抖音样本;未出现的 RP 不应强行虚构案例,而应标记为“待补采/待人工构造测试语料”。",
  1328. "- 因此本版 MD 的置信度提升点在于:每个黄金案例都有真实视频、真实逐字稿上下文、触发词、Rule ID、账号类型、claim 类型、复用判断和边界经验,不再只有摘要结论。",
  1329. "",
  1330. "## 审核口径总览",
  1331. "",
  1332. "| Rule | 默认状态 | 命中逐字稿数 | 核心触发 | 成功边界经验 |",
  1333. "|---|---|---:|---|---|",
  1334. ])
  1335. for row in report["ruleSummary"]:
  1336. rule_id = row["rule"]
  1337. rule_cases = golden_by_rule.get(rule_id, [])
  1338. trigger_samples = []
  1339. for case in rule_cases:
  1340. for item in (case.get("triggerText") or "").split("、"):
  1341. item = clean(item)
  1342. if item and item not in trigger_samples:
  1343. trigger_samples.append(item)
  1344. if len(trigger_samples) >= 5:
  1345. break
  1346. if len(trigger_samples) >= 5:
  1347. break
  1348. trigger_text = "、".join(trigger_samples) or "见逐字稿上下文"
  1349. lines.append(
  1350. f"| {rule_id} {rule_name(rule_id)} | {rule_default_status(rule_id)} | {row['count']} | {snippet(trigger_text, 80)} | {RULE_REWRITE_TEMPLATES.get(rule_id, '回到标签、包装、客服和官方说明口径。')} |"
  1351. )
  1352. lines.extend([
  1353. "",
  1354. "## 未命中规则与补采建议",
  1355. "",
  1356. "以下 RP 在本轮有效逐字稿里没有稳定命中,不代表品牌没有风险,而是当前样本没有足够证据。为了避免“看起来覆盖很全但其实是编造”,本版不为这些 RP 虚构黄金案例。",
  1357. "",
  1358. "| 未命中 RP | 默认状态 | 建议补采方向 |",
  1359. "|---|---|---|",
  1360. ])
  1361. for rule_id in unobserved_rule_ids:
  1362. guide = UNOBSERVED_RULE_SAMPLING_GUIDE.get(
  1363. rule_id,
  1364. "继续采集对应风险场景的视频;如业务需要,可用资料文档红黄绿样例人工构造离线测试集。",
  1365. )
  1366. lines.append(
  1367. f"| {rule_id} {rule_name(rule_id)} | {rule_default_status(rule_id)} | {guide} |"
  1368. )
  1369. lines.extend([
  1370. "",
  1371. "## 品牌级成功经验原则",
  1372. "",
  1373. "| 场景 | 已发布稿件常见边界 | 成功经验 | 评论区承接 |",
  1374. "|---|---|---|---|",
  1375. "| 善存营养包/PRO 营养包 | “一包就够、全部调配好了、明星都在吃、状态变好”会同时触发一包解决感、达人背书和功效承诺。 | 成功稿件通常把它包装成“省心/方便/日常补充/标签建议”,不直接写成治疗或确定改善。 | 置顶说明适用人群、每日建议、版本差异和客服入口。 |",
  1376. "| 善存复合维生素/多维 | “一粒补充 26 种、每天一粒就够、冬天也要爱自己”容易把成分数量推成补全或效果。 | 成功稿件保留“26种/22种/一粒”等事实,但把结果承诺让位给商品页、标签和日常营养补充语境。 | 对“效果怎么样”统一回复为不承诺特定效果,建议结合饮食和作息。 |",
  1377. "| 30+/40+/50+ 人群 | “30+女生、40+精力、老人/爸妈/长辈都适合”容易一概而论。 | 成功稿件把人群作为标签和选购入口,不把人群标签推导为所有人适合或一定有效。 | 用 FAQ 区分 30+/40+/50+、男女版、海外版与国内版。 |",
  1378. "| 海外/跨境/正品 | “海外版更好、国内外通用、大品牌有保障”容易模糊版本、资质和渠道。 | 成功稿件把海外/正品表达放到渠道、包装、喷码、效期和客服承接,不把版本差异说成效果差异。 | 补充真伪查询、效期、版本差异、售后入口。 |",
  1379. "| 直播间促销 | “快冲、拍一发四、数量有限、下单就解决状态”容易把促销和功效绑定。 | 官方/店铺号可承接活动规则;达人号若出现强 CTA,应标记为边界样本,不能作为稳定经验直接复制。 | 置顶购买路径,不在评论区反复承诺功效或价格。 |",
  1380. "",
  1381. "## 话术模块分布",
  1382. "",
  1383. "| 模块 | 有效逐字稿数 |",
  1384. "|---|---:|",
  1385. ])
  1386. for row in report["moduleSummary"]:
  1387. lines.append(f"| {row['module']} | {row['count']} |")
  1388. lines.extend(["", "## Rule Pack 命中分布", "", "| 规则 | 命中逐字稿数 |", "|---|---:|"])
  1389. for row in report["ruleSummary"]:
  1390. lines.append(f"| {row['rule']} | {row['count']} |")
  1391. lines.extend([
  1392. "",
  1393. "## Rule Pack 逐条复盘",
  1394. "",
  1395. "这一节把资料文档里的红黄绿思路翻译成善存抖音口播的真实场景。每个 RP 先给判断口径,再列出本轮逐字稿里最有代表性的 3 条触发样本,方便后续和审核、法务或达人团队逐条校准。",
  1396. "",
  1397. ])
  1398. for row in report["ruleSummary"]:
  1399. rule_id = row["rule"]
  1400. rule_cases = golden_by_rule.get(rule_id, [])
  1401. lines.extend([
  1402. f"### {rule_id}|{rule_name(rule_id)}",
  1403. "",
  1404. f"- 默认动作:{rule_default_status(rule_id)},{rule_audit_action(rule_id)}。",
  1405. f"- 本轮命中:{row['count']} 条有效逐字稿;高频模块:{RULE_MODULE_MAP.get(rule_id, '按上下文判断')}。",
  1406. f"- 边界经验:{RULE_REWRITE_TEMPLATES.get(rule_id, '回到标签、包装、客服和官方说明口径。')}",
  1407. "",
  1408. "| 案例ID | 账号类型 | 来源 | 触发片段 | 可迁移经验 |",
  1409. "|---|---|---|---|---|",
  1410. ])
  1411. for case in rule_cases[:3]:
  1412. lines.append(
  1413. f"| {case['id']} | {case.get('accountType', '')} | {case['author']}:{snippet(case['sourceTitle'], 36)} | {snippet(case.get('triggerText') or case['originalTranscriptSnippet'], 90)} | {snippet(case.get('successExperience', ''), 140)} |"
  1414. )
  1415. lines.append("")
  1416. lines.extend([
  1417. "",
  1418. f"## 逐字稿成功边界案例集({s.get('goldenTranscriptCount', 0)} 行)",
  1419. "",
  1420. "说明:以下不是按视频抽 12 条,也不是给待审稿写“建议通过版”;它按“有效逐字稿 × Rule Pack 命中”拆行,用已发布内容反推成功边界。同一条视频同时命中多个 RP 时,会沉淀为多条可训练、可复核的边界案例。",
  1421. "",
  1422. "| ID | Rule | 账号类型 | claim类型 | 复用判断 | 来源 | 触发片段 | 边界逻辑/成功经验 |",
  1423. "|---|---|---|---|---|---|---|---|",
  1424. ])
  1425. for row in report["goldenTranscriptRows"]:
  1426. lines.append(
  1427. f"| {row['id']} | {row.get('ruleId', row['riskRules'])} {row.get('ruleName', '')} | {row.get('accountType', '')} | {row.get('claimType', '')} | {row.get('reusability', '')} | {row['author']}:{snippet(row['sourceTitle'], 32)} | {snippet(row.get('triggerText', ''), 60)} | {snippet(row.get('successExperience', ''), 180)} |"
  1428. )
  1429. lines.extend(["", "## 重点逐字稿样本", ""])
  1430. for row in sorted([r for r in report["transcriptRows"] if r["validTranscript"]], key=lambda r: (r["likeCount"], r["commentCount"]), reverse=True)[:18]:
  1431. lines.extend([
  1432. f"### {row['author']}|{snippet(row['title'], 50)}",
  1433. "",
  1434. f"- 视频 ID:{row['videoId']};互动:{row['likeCount']} 赞 / {row['commentCount']} 评论;SKU:{row['skuGroup']}",
  1435. f"- 账号类型:{row.get('accountType', '')};claim 类型:{row.get('claimType', '')};边界逻辑:{row.get('boundaryLogic', '')}",
  1436. f"- 话术模块:{row['moduleText']}",
  1437. f"- 命中规则:{row['ruleHits']}({row['ruleNames']})",
  1438. f"- 逐字稿摘录:{snippet(row['cleanText'], 260)}",
  1439. f"- 成功经验观察:{account_rule_notes(row.get('accountType', ''))} {row.get('claimCarrier', '')}",
  1440. "",
  1441. ])
  1442. lines.extend([
  1443. "## 评论改进空间复核",
  1444. "",
  1445. "| 评论主题 | 评论数 | 典型评论 | 口播诱因 | 改进动作 | 置顶/客服回复建议 |",
  1446. "|---|---:|---|---|---|---|",
  1447. ])
  1448. for row in report["commentThemeSummary"]:
  1449. sample_comments = " / ".join(snippet(item, 32) for item in (row.get("sampleComments") or [])[:3])
  1450. lines.append(
  1451. f"| {row['theme']} | {row['commentCount']} | {sample_comments} | {COMMENT_TRANSCRIPT_TRIGGERS.get(row['theme'], '由口播、标题、标签和商品页共同触发,需结合视频上下文复核。')} | {row['opportunity']} | {COMMENT_REPLY_TEMPLATES.get(row['theme'], '先确认用户真实问题,再回到商品页、包装标签和客服说明。')} |"
  1452. )
  1453. lines.extend([
  1454. "",
  1455. "逐字稿确认用户评论里的疑问并不是孤立售后问题,而是被口播触发:当口播强化“就这一包/一粒就够/明星都在吃/状态变好/直播间快冲”时,评论区自然追问功效、正品、版本、适用人群和购买路径。后续投放建议同步改三处:口播降风险、置顶评论答疑、商品卡/客服承接版本和标签信息。",
  1456. "",
  1457. "评论区不建议用“亲测有效、放心吃、适合所有人、闭眼入、快冲就完事”这类短回复补火。更稳的做法是把评论分成三类:真实购买疑问交给商品页/客服,功效健康诉求回到日常营养补充和专业咨询,低质互动只做轻回应或不回应。",
  1458. "",
  1459. "## 官方/店铺账号清单对照",
  1460. "",
  1461. "以下来自 `店铺与账号.xlsx`,用于本轮账号类型判定。清单命中代表“可按官方/店铺账号逻辑分析”,但不自动等同于所有功能 claim 都能说;仍需匹配具体产品批文、广审备案和平台发布身份。",
  1462. "",
  1463. "| 店铺 | 直播账号 | 抖音号ID |",
  1464. "|---|---|---|",
  1465. ])
  1466. for row in report.get("officialAccountRecords", []):
  1467. lines.append(f"| {row.get('store', '')} | {row.get('liveAccount', '')} | {row.get('douyinId', '')} |")
  1468. lines.extend([
  1469. "",
  1470. "## 自检清单(发布前逐条核验)",
  1471. "",
  1472. "这份清单用于把黄金数据集变成审核 SOP:每条脚本、标题、字幕、商品卡和评论区回复发布前都按同一顺序扫一遍。命中红灯项时,不建议靠谐音、错别字或画面暗示绕审;应回到产品资料、标签、广审和账号资质。",
  1473. "",
  1474. "| 序号 | 核验问题 | 处理动作 |",
  1475. "|---:|---|---|",
  1476. ])
  1477. for row in SELF_CHECK_ROWS:
  1478. lines.append(f"| {row[0]} | {row[1]} | {row[2]} |")
  1479. lines.extend([
  1480. "",
  1481. "## 下一步建议",
  1482. "",
  1483. "1. 继续多跑样本时优先按“官方/店铺账号、疑似店铺账号、达人账号”分层采集,避免把不同账号身份的成功经验混在一起。",
  1484. "2. 对每条新增样本都打 `账号身份 × 产品属性 × 卖点类型 × 人群标签 × 功效 claim × 承载方式 × 购买引导强度` 标签,累计成可筛选的策略公式库。",
  1485. "3. 对官方/店铺账号样本补齐产品批文、保健食品功能、广审备案范围和商品页信息,区分“备案内绿灯表达”和“超范围隐喻/撞审表达”。",
  1486. "4. 对达人样本重点观察隐喻、生活场景、弱化词、个人体验和非强导购路径,形成达人 brief 里的可复制经验与禁用边界。",
  1487. "5. 对银善存/大脑/脑力类 claim 另建专题:区分直接功能宣称、画面隐喻、角色比喻和备案范围,复核“守门员/前锋”等画面话术为什么能发。",
  1488. ])
  1489. return "\n".join(lines)
  1490. if __name__ == "__main__":
  1491. main()