| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- import json
- import math
- import re
- from collections import Counter, defaultdict
- from datetime import datetime, timezone
- from pathlib import Path
- OUTPUT_DIR = Path("outputs/centrum_douyin_audit_2026-06-08/live-full")
- INPUT_FILES = [
- OUTPUT_DIR / "live-raw-dataset.json",
- OUTPUT_DIR / "live-raw-dataset-batch2.json",
- ]
- RULE_TEXT = Path("outputs/centrum_douyin_audit_2026-06-08/rule_pack_extracted.txt")
- def clean(value):
- return re.sub(r"\s+", " ", str(value or "")).strip()
- def as_int(value):
- try:
- number = int(float(value or 0))
- except Exception:
- number = 0
- return number
- def load_json(path):
- return json.loads(path.read_text(encoding="utf-8"))
- def video_score(video):
- return (
- as_int(video.get("likeCount"))
- + as_int(video.get("commentCount")) * 3
- + as_int(video.get("shareCount")) * 2
- + as_int(video.get("playCount")) * 0.02
- )
- def video_key(video):
- return clean(video.get("id") or video.get("videoId") or video.get("url"))
- def comment_key(comment):
- return clean(comment.get("id") or comment.get("cid") or f"{comment.get('videoId')}:{comment.get('text')}")
- def merge_datasets():
- videos = {}
- comments = {}
- warnings = []
- errors = []
- effective_keywords = []
- requested_keywords = []
- for path in INPUT_FILES:
- if not path.exists():
- continue
- data = load_json(path)
- requested_keywords.extend(data.get("profile", {}).get("keywords", []))
- effective_keywords.extend(data.get("effectiveKeywords", []))
- warnings.extend(data.get("warnings", []))
- errors.extend(data.get("errors", []))
- for video in data.get("videos", []):
- key = video_key(video)
- if not key:
- continue
- if key not in videos or video_score(video) > video_score(videos[key]):
- videos[key] = video
- for comment in data.get("comments", []):
- key = comment_key(comment)
- if key and key not in comments:
- comments[key] = comment
- return {
- "videos": list(videos.values()),
- "comments": list(comments.values()),
- "warnings": warnings,
- "errors": errors,
- "requestedKeywords": sorted(set(map(clean, requested_keywords)) - {""}),
- "effectiveKeywords": sorted(set(map(clean, effective_keywords)) - {""}),
- }
- SKU_RULES = [
- ("善存PRO/每日营养包", r"PRO营养包|每日营养包|营养包|代谢包|状态全开|气血满格|新生焕活"),
- ("银善存/脑力瓶", r"银善存|脑力瓶|大脑维生素|脑力|大脑"),
- ("善存复合维生素/多维", r"复合维生素|多维|维生素|多维元素|26种营养"),
- ("善存男士/女士/50+", r"男士|女士|女性|男性|50岁|50\+|中老年|老人|Costco|美国"),
- ("善存小佳维/儿童", r"小佳维|儿童|孩子|宝宝|长高"),
- ("善存孕妇复合维生素", r"孕妇|孕期|备孕|叶酸|胎儿"),
- ("善存海外/跨境版本", r"海外|跨境|澳洲|美国版|澳版|天猫国际|京东国际|包邮"),
- ]
- RULE_PATTERNS = {
- "RP01": ("绝对化/保证性/最高级", r"全网第一|第一|唯一|100%|保证|最[好强]|立刻|马上|闭眼入|爆火"),
- "RP02": ("疾病治疗/治愈/诊断暗示", r"甲流|感冒|肿瘤|医生|治疗|治好|预防|缓解|疼|病|症|免疫"),
- "RP03": ("未经确认功效/健康利益宣称", r"精力|脑力|大脑|气血|代谢|瘦|吃瘦|状态|焕活|免疫力|长高|睡眠|抗疲劳"),
- "RP04": ("数字/实验/前后对比造成明确功效承诺", r"\d+\s*(天|周|月|%|倍|种|粒|岁)|提升|改善|对比|实测|检测"),
- "RP05": ("产品法规属性误用/三品一械边界错误", r"海外|跨境|保健|蓝帽|普通食品|美国|澳洲|Costco|天猫国际|京东国际"),
- "RP06": ("包装/标签/备案/说明书不一致", r"正品|真假|喷码|批号|效期|保质期|版本|包装|标签"),
- "RP07": ("用量/用法/禁忌/适用人群误导", r"每天|一粒|几粒|怎么吃|吃法|空腹|饭后|孕妇|儿童|孩子|老人|全家|适合"),
- "RP11": ("竞品贬损/虚假比较/不公平竞争", r"红黑榜|黑榜|智商税|别买|不如|对比|哪家|排名|踩坑"),
- "RP13": ("表达强度过高但可弱化", r"满格|全开|必看|不行|不能少|值得拥有|还不快|妙招"),
- "RP14": ("证据引用不足或上下文不清", r"营养师|专家|医生|高管|实测|研究|标准|干货|教你"),
- "RP16": ("平台敏感词/卡审词疑似命中", r"甲流|吃瘦|瘦|气血|脑力|大脑|病|医生|减肥"),
- "RP18": ("促销/价格/库存/券后信息不清", r"活动|包邮|直播|京东|天猫|券|价格|优惠|买|下单"),
- "RP19": ("达人/KOL/KOC/专家身份表达边界", r"营养师|专家|医生|高管|主持人|明星|教你"),
- "RP21": ("CTA/购买引导/场景导向不清", r"买|下单|链接|购物车|看看|拥有|囤|包邮|直播"),
- "RP22": ("灰区创意/趋势模仿/情绪钩子", r"必看|改命|焦虑|竞争力|长续婚姻|身材管理|小妙招|闭眼入"),
- }
- COMMENT_THEMES = [
- ("低质互动/刷金币", r"金币|坚持互动|多互动|串门|涨粉|许愿|抖音大大|满减券|6000|7000|5000|3000|稳定得|互关|看广告|下图回复"),
- ("购买路径/价格", r"怎么买|哪里买|链接|购物车|多少钱|价格|贵|便宜|活动|券|京东|天猫|直播|下单|店铺"),
- ("正品/渠道/版本信任", r"正品|真假|喷码|批号|效期|保质期|过期|版本|美国版|澳洲|海外|进口|天猫国际|京东国际"),
- ("用法用量/适用人群", r"怎么吃|几粒|一天吃|每天吃|饭前|饭后|空腹|孕妇|儿童|孩子|老人|哺乳|妈妈能吃|适合.*(孕妇|儿童|老人|孩子|中老年)"),
- ("功效/健康诉求", r"有用|效果|精力|气血|代谢|免疫|甲流|睡眠|脑力|瘦|长高|补什么|缺"),
- ("体验/副作用/剂型", r"反胃|恶心|吞|吞咽|颗粒|大粒|便秘|拉肚子|不舒服|味道|腥"),
- ("质疑/比较/性价比", r"智商税|真的|靠谱吗|有必要|不如|对比|区别|性价比|划算|坑|贵"),
- ("内容互动/达人明星", r"喜欢|好看|姐姐|明星|秋瓷炫|颖儿|维嘉|百克力|老师|收藏|学到了"),
- ]
- def classify_sku(text):
- hits = [label for label, pattern in SKU_RULES if re.search(pattern, text, flags=re.I)]
- return hits or ["善存泛品牌/其他"]
- def classify_rules(text):
- hits = []
- for rule_id, (name, pattern) in RULE_PATTERNS.items():
- if re.search(pattern, text, flags=re.I):
- hits.append({"ruleId": rule_id, "ruleName": name})
- return hits
- def classify_comment(text):
- if re.search(COMMENT_THEMES[0][1], text, flags=re.I):
- return [COMMENT_THEMES[0][0]]
- hits = [label for label, pattern in COMMENT_THEMES if re.search(pattern, text, flags=re.I)]
- return hits or ["泛互动/情绪反馈"]
- def risk_level(rule_ids):
- red = {"RP01", "RP02", "RP03", "RP04", "RP05", "RP06", "RP07", "RP11"}
- if len(red.intersection(rule_ids)) >= 3:
- return "高"
- if red.intersection(rule_ids):
- return "中高"
- if rule_ids:
- return "中"
- return "低"
- def truncate(text, length=120):
- text = clean(text)
- return text if len(text) <= length else text[: length - 1] + "…"
- def comment_rows(comments, videos_by_id):
- rows = []
- for comment in comments:
- text = clean(comment.get("text"))
- if not text:
- continue
- video = videos_by_id.get(clean(comment.get("videoId")), {})
- themes = classify_comment(text)
- rows.append({
- "commentId": comment.get("id") or comment.get("cid") or "",
- "videoId": comment.get("videoId") or "",
- "videoTitle": video.get("title") or "",
- "videoAuthor": video.get("author") or "",
- "keyword": comment.get("keyword") or video.get("keyword") or "",
- "themes": themes,
- "themeText": "、".join(themes),
- "text": text,
- "likeCount": as_int(comment.get("likeCount")),
- "replyCount": as_int(comment.get("replyCommentTotal")),
- "ipLocation": comment.get("ipLocation") or "",
- })
- rows.sort(key=lambda row: (row["likeCount"], len(row["text"])), reverse=True)
- return rows
- def analyze_videos(videos, comments_by_video):
- rows = []
- for video in videos:
- text = clean(" ".join([
- video.get("title"),
- video.get("desc"),
- video.get("content"),
- " ".join(video.get("tags") or []),
- ]))
- sku_hits = classify_sku(text)
- rule_hits = classify_rules(text)
- rule_ids = [item["ruleId"] for item in rule_hits]
- comments = comments_by_video.get(clean(video.get("id")), [])
- top_comment = sorted(comments, key=lambda c: as_int(c.get("likeCount")), reverse=True)[:1]
- rows.append({
- "videoId": video.get("id") or "",
- "keyword": video.get("keyword") or "",
- "skuGroup": "、".join(sku_hits),
- "author": video.get("author") or "",
- "title": clean(video.get("title") or video.get("desc")),
- "publishTime": video.get("timestamp") or "",
- "likeCount": as_int(video.get("likeCount")),
- "commentCount": as_int(video.get("commentCount")),
- "shareCount": as_int(video.get("shareCount")),
- "collectCount": as_int(video.get("collectCount")),
- "playCount": as_int(video.get("playCount")),
- "score": round(video_score(video), 2),
- "url": video.get("url") or video.get("sourceUrl") or video.get("fallbackUrl") or "",
- "coverUrl": video.get("coverUrl") or "",
- "tags": "、".join(video.get("tags") or []),
- "ruleHits": "、".join(rule_ids),
- "ruleNames": "、".join([item["ruleName"] for item in rule_hits]),
- "riskLevel": risk_level(set(rule_ids)),
- "commentCaptured": len(comments),
- "topComment": top_comment[0].get("text") if top_comment else "",
- "topCommentLike": as_int(top_comment[0].get("likeCount")) if top_comment else 0,
- })
- rows.sort(key=lambda row: row["score"], reverse=True)
- return rows
- def build_theme_summary(comment_data):
- buckets = defaultdict(list)
- for row in comment_data:
- for theme in row["themes"]:
- buckets[theme].append(row)
- summaries = []
- for theme, rows in buckets.items():
- sorted_rows = sorted(rows, key=lambda r: r["likeCount"], reverse=True)
- examples = [r["text"] for r in sorted_rows[:5]]
- summaries.append({
- "theme": theme,
- "commentCount": len(rows),
- "likedCommentCount": sum(1 for row in rows if row["likeCount"] > 0),
- "totalLikes": sum(row["likeCount"] for row in rows),
- "sampleComments": examples,
- "opportunity": comment_opportunity(theme),
- })
- summaries.sort(key=lambda row: (row["commentCount"], row["totalLikes"]), reverse=True)
- return summaries
- def comment_opportunity(theme):
- mapping = {
- "购买路径/价格": "置顶评论和商品卡要明确官方购买路径、活动口径和客服入口,避免用户在评论区反复问链接/价格。",
- "正品/渠道/版本信任": "补充正品辨别、版本差异、喷码/批号/效期说明,跨境 SKU 尤其要把包装版本说清楚。",
- "用法用量/适用人群": "所有用法、用量、特殊人群问题都回到标签/说明书,并引导咨询专业人士。",
- "功效/健康诉求": "用户会把内容理解成功效承诺,后续稿件应减少结果承诺,改为日常营养支持和生活方式组合。",
- "体验/副作用/剂型": "建立颗粒大小、吞咽、服用时间、胃部不适等体验 FAQ,但不要自行给医疗建议。",
- "质疑/比较/性价比": "用成分、剂型、预算、版本和适用场景做中性比较,避免红黑榜和竞品贬损。",
- "内容互动/达人明星": "高互动来自达人/情绪内容时,要用评论区和商品卡补足 SKU 承接,否则曝光不等于转化。",
- "泛互动/情绪反馈": "保留情绪互动价值,同时在高赞评论下补充产品边界与购买答疑。",
- }
- return mapping.get(theme, "需要人工复核评论上下文后再定动作。")
- def build_goldens():
- return [
- {
- "id": "GD-LIVE-01",
- "riskPrototype": "左右脑互博、40+精力满格、脑力/精力状态承诺",
- "ruleHits": "RP02、RP03、RP13、RP16",
- "beforeExample": "梗王蓄力局,40+精力满格 #善存PRO营养包",
- "afterExample": "40+忙碌阶段更要关注饮食、作息和日常营养补充;产品请按标签建议选择。",
- "why": "保留年龄场景和生活状态,但不承诺精力/脑力改善。",
- "sourceEvidence": "李维嘉/善存PRO营养包 live 样本",
- },
- {
- "id": "GD-LIVE-02",
- "riskPrototype": "吃瘦不饿瘦、身材管理小妙招、代谢包",
- "ruleHits": "RP03、RP04、RP13、RP16",
- "beforeExample": "开工前我的身材管理小妙招来啦 #善存代谢包 #状态全开",
- "afterExample": "开工阶段也别忽略均衡饮食和规律运动;如需补充维生素矿物质,请按产品标签建议。",
- "why": "删除减重/代谢结果暗示,把内容改为生活方式+日常补充。",
- "sourceEvidence": "颖儿/善存代谢包 live 样本",
- },
- {
- "id": "GD-LIVE-03",
- "riskPrototype": "气血满格、新生焕活,就这一包",
- "ruleHits": "RP03、RP13、RP16",
- "beforeExample": "气血满格,就这一包;新生焕活,就这一包。",
- "afterExample": "忙碌和年龄阶段可以关注日常营养管理,按标签建议补充相关营养成分。",
- "why": "弱化强感受和单包解决感,减少平台卡审和功效风险。",
- "sourceEvidence": "善存海外旗舰店/PRO营养包 live 样本",
- },
- {
- "id": "GD-LIVE-04",
- "riskPrototype": "甲流有哪些维生素可以吃",
- "ruleHits": "RP02、RP03、RP16",
- "beforeExample": "甲流有哪些维生素可以吃?一粒补充26种营养。",
- "afterExample": "特殊健康时期更要注意均衡饮食和休息;本品为日常营养补充,不替代药物或治疗。",
- "why": "删除疾病场景和营养品解决病症的关联。",
- "sourceEvidence": "善存复合维生素 live 样本",
- },
- {
- "id": "GD-LIVE-05",
- "riskPrototype": "一粒就可以补充26人体所需营养/一粒补全",
- "ruleHits": "RP01、RP03、RP04、RP07",
- "beforeExample": "一粒就可以补充26人体所需营养。",
- "afterExample": "含多种维生素和矿物质,适合按产品标签建议作为日常营养补充。",
- "why": "保留成分数量,但不推导全部所需或一粒补全。",
- "sourceEvidence": "善存复合维生素 live 样本",
- },
- {
- "id": "GD-LIVE-06",
- "riskPrototype": "国外国内爆火、海外好物闭眼入",
- "ruleHits": "RP01、RP05、RP13、RP18",
- "beforeExample": "国外国内爆火的善存女性维生素,海外好物闭眼入。",
- "afterExample": "不同渠道/版本的包装和说明可能不同,请以当前购买页面、包装标签和官方客服说明为准。",
- "why": "把热销背书和跨境信任改为版本与渠道说明。",
- "sourceEvidence": "善存海外/女士维生素 live 样本",
- },
- {
- "id": "GD-LIVE-07",
- "riskPrototype": "成人儿童营养好物、不花冤枉钱的泛比较",
- "ruleHits": "RP07、RP11、RP14",
- "beforeExample": "成人&儿童营养好物,看了不花冤枉钱。",
- "afterExample": "成人和儿童适用产品不同,建议按年龄、标签说明和自身需求选择。",
- "why": "避免成人儿童泛化和价值判断过强。",
- "sourceEvidence": "营养师/山姆营养品区 live 样本",
- },
- {
- "id": "GD-LIVE-08",
- "riskPrototype": "正品/喷码/效期/版本问题散落在评论区",
- "ruleHits": "RP05、RP06、RP18、RP21",
- "beforeExample": "评论区反复追问真假、效期、喷码、美国版/澳洲版区别。",
- "afterExample": "置顶评论:购买请认准官方授权渠道;不同版本包装/成分/适用说明请以购买页和实物标签为准,效期/喷码可咨询客服。",
- "why": "将售前信任问题前置,降低投诉和客服压力。",
- "sourceEvidence": "多条善存海外/男士/女士/50+评论",
- },
- {
- "id": "GD-LIVE-09",
- "riskPrototype": "明星都在吃/达人高管背书",
- "ruleHits": "RP14、RP19、RP22",
- "beforeExample": "秋瓷炫、王艳、百克力都在吃的营养包。",
- "afterExample": "达人分享仅代表个人体验;产品信息以包装、标签和官方说明为准。",
- "why": "保留达人种草,但不把身份/使用行为变成效果背书。",
- "sourceEvidence": "善存海外旗舰店/营养包 live 样本",
- },
- {
- "id": "GD-LIVE-10",
- "riskPrototype": "50岁以上善存怎么选、男士/女士/海外版混讲",
- "ruleHits": "RP05、RP06、RP07、RP14",
- "beforeExample": "美国 Costco 三款 50 岁以上善存如何选择。",
- "afterExample": "不同年龄和版本适用说明不同,请按具体 SKU、包装标签和购买渠道说明选择。",
- "why": "SKU/版本边界清楚,避免跨版本套用。",
- "sourceEvidence": "50+版本选择 live 样本",
- },
- ]
- def build_script_breakdowns(top_videos):
- rows = []
- for video in top_videos[:30]:
- title = video["title"]
- rules = set(filter(None, video["ruleHits"].split("、")))
- if re.search(r"参鸡汤|餐厅|身材管理|开工|婚姻|阅读|高管|梗王", title):
- hook = "生活方式/达人故事钩子"
- structure = "先用生活或人物情境吸引停留,再用标签、话题或轻口播承接善存 SKU。"
- fix = "保留故事,但结尾补 SKU 边界:日常营养补充、按标签建议、非治疗。"
- elif re.search(r"甲流|维生素可以吃|医生|营养师", title):
- hook = "专家/健康问题钩子"
- structure = "用健康问题或专业身份制造可信度。"
- fix = "删疾病解决路径,改成均衡饮食与标签范围;专家身份不替代证据。"
- elif re.search(r"正品|版本|Costco|怎么选|选择", title):
- hook = "选购/信任答疑钩子"
- structure = "围绕版本、渠道、真假、效期等购买前疑问展开。"
- fix = "适合做置顶 FAQ 和客服承接,减少评论区重复追问。"
- else:
- hook = "商品卖点/活动钩子"
- structure = "用成分数量、活动、包邮或热门标签推动转化。"
- fix = "保留成分和促销信息,但避免一粒补全、保证效果、闭眼入。"
- rows.append({
- "videoId": video["videoId"],
- "title": title,
- "author": video["author"],
- "skuGroup": video["skuGroup"],
- "hookType": hook,
- "currentStructure": structure,
- "riskRules": "、".join(sorted(rules)),
- "reusableFix": fix,
- })
- return rows
- def build_summary(video_rows, comment_data, theme_summary, merged):
- sku_counts = Counter()
- for row in video_rows:
- for sku in row["skuGroup"].split("、"):
- sku_counts[sku] += 1
- risk_counts = Counter(row["riskLevel"] for row in video_rows)
- author_counts = Counter(row["author"] for row in video_rows if row["author"])
- keyword_counts = Counter(row["keyword"] for row in video_rows if row["keyword"])
- return {
- "generatedAt": datetime.now(timezone.utc).astimezone().isoformat(),
- "requestedKeywordCount": len(merged["requestedKeywords"]),
- "effectiveKeywordCount": len(merged["effectiveKeywords"]),
- "videoCount": len(video_rows),
- "commentCount": len(comment_data),
- "actionableCommentCount": sum(1 for row in comment_data if "低质互动/刷金币" not in row["themes"]),
- "lowQualityCommentCount": sum(1 for row in comment_data if "低质互动/刷金币" in row["themes"]),
- "warningCount": len(merged["warnings"]),
- "errorCount": len(merged["errors"]),
- "skuCounts": [{"skuGroup": k, "videoCount": v} for k, v in sku_counts.most_common()],
- "riskCounts": [{"riskLevel": k, "videoCount": v} for k, v in risk_counts.most_common()],
- "topAuthors": [{"author": k, "videoCount": v} for k, v in author_counts.most_common(15)],
- "topKeywords": [{"keyword": k, "videoCount": v} for k, v in keyword_counts.most_common(20)],
- "topCommentThemes": theme_summary[:10],
- }
- def build_markdown(data):
- summary = data["summary"]
- top_videos = data["videoRows"][:18]
- top_themes = data["themeSummary"][:8]
- goldens = data["goldenRows"]
- script_rows = data["scriptBreakdowns"][:12]
- lines = []
- lines.append("# 善存抖音稿件审核润色与评论 VOC Live 版报告")
- lines.append("")
- lines.append(f"生成时间:{summary['generatedAt']}")
- lines.append("")
- lines.append("## 样本覆盖")
- lines.append("")
- lines.append(f"- live 去重视频样本:{summary['videoCount']} 条")
- lines.append(f"- live 去重评论样本:{summary['commentCount']} 条")
- lines.append(f"- 可行动 VOC 评论:{summary['actionableCommentCount']} 条;低质互动/刷金币类评论:{summary['lowQualityCommentCount']} 条")
- lines.append(f"- 请求关键词:{summary['requestedKeywordCount']} 个;有效返回关键词:{summary['effectiveKeywordCount']} 个")
- lines.append(f"- 接口警告/错误:{summary['warningCount']} / {summary['errorCount']},主要为部分关键词或单条评论页 fetch failed;已保留成功样本。")
- lines.append("- 规则资料:QW1 Rule Pack Examples V0.1,已提取 28 个 Rule Pack,用于规则映射和黄金改写。")
- lines.append("")
- lines.append("## 方法说明与限制")
- lines.append("")
- lines.append("- 采集口径:使用 `claude-code-voc-intelligence` 的抖音采集能力,围绕善存、善存营养包、复合维生素、30+/50+、男士/女士、海外版/跨境版等关键词抓取视频与评论,并对跨批次视频、评论做去重。")
- lines.append("- 脚本拆解口径:使用 `douyin-speaking-daily` 的稿件拆解能力,对标题、标签、描述、评论和可获取文本形成 transcript proxy 与脚本结构分析;未做完整视频 ASR 的样本,结论以可见文案和评论为准。")
- lines.append("- 审核差异限制:本轮未取得品牌审核前原稿、驳回原因、修改版本流水和最终通过稿的一一对应关系,因此“做了哪些润色和修改”是基于已发布稿件、Rule Pack 命中风险和可复用通过话术的反推,不等同于逐条原稿差异审计。")
- lines.append("- 证据强度:78 条视频和 471 条评论足以支撑本轮方向性判断;涉及合规结论时仍建议由品牌法务/平台审核团队结合原始投放链路复核。")
- lines.append("")
- lines.append("## 一句话结论")
- lines.append("")
- lines.append("善存抖音已投内容的主流过审路径不是直接讲“治疗/功效”,而是把产品包进明星生活方式、职场状态、选购答疑、跨境版本说明和日常营养补充场景中;但 live 样本里仍大量出现“精力满格、气血满格、代谢/吃瘦、甲流、脑力/大脑、闭眼入、一粒补充所需”等高风险表达。评论区最值得改进的是:正品与版本信任、用法用量/适用人群、购买路径/价格、功效追问和剂型体验。")
- lines.append("")
- lines.append("## 已发布稿件的润色/修改路径")
- lines.append("")
- lines.append("1. 明星生活方式化:秋瓷炫参鸡汤面、颖儿开工身材管理、李维嘉 40+梗王等内容,把产品从硬功效改为生活情境和标签承接。")
- lines.append("2. 年龄/状态场景化:PRO 营养包常用 30+/40+、精力、气血、状态等表达,过审上比治疗承诺温和,但仍需进一步弱化为日常营养管理。")
- lines.append("3. 店铺商品话术成分化:官方/旗舰店内容常用“26种营养、一粒、男女复合维生素、海外版”等成分和版本话术,需避免推导“一粒补全/疾病时期该吃”。")
- lines.append("4. 跨境 SKU 信任化:海外/美国/澳洲/Costco/天猫国际相关内容更适合做版本、包装、渠道和效期说明,而不是用海外身份做安全背书。")
- lines.append("5. 评论承接客服化:正品、喷码、效期、怎么吃、适合谁、哪里买等评论应前置成置顶 FAQ 和客服关键词库。")
- lines.append("")
- lines.append("## 黄金改写数据集")
- lines.append("")
- lines.append("| ID | 风险原型 | 命中规则 | 风险表达 | 建议通过版 | 依据 |")
- lines.append("|---|---|---|---|---|---|")
- for row in goldens:
- lines.append(f"| {row['id']} | {row['riskPrototype']} | {row['ruleHits']} | {row['beforeExample']} | {row['afterExample']} | {row['why']} |")
- lines.append("")
- lines.append("## 评论 VOC 主题与改进空间")
- lines.append("")
- lines.append("| 主题 | 评论数 | 点赞评论数 | 高赞/典型评论摘录 | 改进动作 |")
- lines.append("|---|---:|---:|---|---|")
- for row in top_themes:
- examples = "<br>".join(truncate(text, 60) for text in row["sampleComments"][:3])
- lines.append(f"| {row['theme']} | {row['commentCount']} | {row['likedCommentCount']} | {examples} | {row['opportunity']} |")
- lines.append("")
- lines.append("## 样本案例 Top")
- lines.append("")
- for index, row in enumerate(top_videos, start=1):
- lines.append(f"### {index}. {row['title'] or row['videoId']}")
- lines.append(f"- 作者:{row['author']}|关键词:{row['keyword']}|SKU:{row['skuGroup']}")
- lines.append(f"- 互动:{row['likeCount']}赞 / {row['commentCount']}评 / {row['shareCount']}转 / {row['collectCount']}藏")
- lines.append(f"- 规则命中:{row['ruleHits'] or '未命中主要规则'}|风险等级:{row['riskLevel']}")
- lines.append(f"- 链接:{row['url']}")
- if row["topComment"]:
- lines.append(f"- 高赞评论:{truncate(row['topComment'], 90)}({row['topCommentLike']}赞)")
- lines.append("")
- lines.append("## 逐字稿/脚本结构拆解")
- lines.append("")
- lines.append("本轮未批量下载视频做 ASR,逐字稿拆解使用 live 采集到的视频标题、描述、标签和评论作为脚本代理文本,并用本地 douyin-viral-script-analyzer 跑通结构拆解。正式复盘审核前后稿时,可把原始口播 ASR 替换进同一输入结构。")
- lines.append("")
- lines.append("| 样本 | 钩子类型 | 当前结构 | 风险规则 | 可复用改法 |")
- lines.append("|---|---|---|---|---|")
- for row in script_rows:
- lines.append(f"| {truncate(row['title'], 36)} | {row['hookType']} | {row['currentStructure']} | {row['riskRules']} | {row['reusableFix']} |")
- lines.append("")
- lines.append("## 下一步建议")
- lines.append("")
- lines.append("1. 把品牌审核前原稿、驳回原因和通过稿补入 `content_id/version_id/audit_id`,即可把本报告从“已发布稿件推断”升级为真正的审核前后差异复盘。")
- lines.append("2. 对评论数最高的官方/达人稿件追加 ASR,尤其是秋瓷炫、颖儿、李维嘉、善存海外旗舰店和 50+版本选择内容。")
- lines.append("3. 建立置顶评论模板:正品/版本/效期、用法用量、特殊人群、非治疗声明、购买路径。")
- return "\n".join(lines)
- def main():
- OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
- merged = merge_datasets()
- comments_by_video = defaultdict(list)
- for comment in merged["comments"]:
- comments_by_video[clean(comment.get("videoId"))].append(comment)
- videos_by_id = {clean(video.get("id")): video for video in merged["videos"]}
- video_rows = analyze_videos(merged["videos"], comments_by_video)
- comment_data = comment_rows(merged["comments"], videos_by_id)
- theme_summary = build_theme_summary(comment_data)
- summary = build_summary(video_rows, comment_data, theme_summary, merged)
- script_breakdowns = build_script_breakdowns(video_rows)
- data = {
- "summary": summary,
- "requestedKeywords": merged["requestedKeywords"],
- "effectiveKeywords": merged["effectiveKeywords"],
- "videoRows": video_rows,
- "commentRows": comment_data,
- "themeSummary": theme_summary,
- "goldenRows": build_goldens(),
- "scriptBreakdowns": script_breakdowns,
- "warnings": merged["warnings"],
- "errors": merged["errors"],
- "rulePackSource": str(RULE_TEXT),
- }
- (OUTPUT_DIR / "live-analysis-data.json").write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
- (OUTPUT_DIR / "善存抖音稿件审核润色与评论VOC-live.md").write_text(build_markdown(data), encoding="utf-8")
- analyzer_input = {
- "metadata": {"project": "善存抖音稿件审核润色与评论VOC-live", "generatedAt": summary["generatedAt"]},
- "videos": [
- {
- "aweme_id": row["videoId"],
- "keyword": row["keyword"],
- "title": row["title"],
- "author": {"nickname": row["author"]},
- "likeCount": row["likeCount"],
- "commentCount": row["commentCount"],
- "shareCount": row["shareCount"],
- "playCount": row["playCount"],
- "url": row["url"],
- }
- for row in video_rows
- ],
- "comments": [
- {
- "id": row["commentId"],
- "aweme_id": row["videoId"],
- "text": row["text"],
- "likeCount": row["likeCount"],
- }
- for row in comment_data
- ],
- "transcripts": [
- {
- "awemeId": row["videoId"],
- "provider": "live-title-desc-proxy",
- "text": "。".join(filter(None, [row["title"], row["topComment"]])),
- "segments": [],
- }
- for row in video_rows
- ],
- }
- (OUTPUT_DIR / "douyin_analyzer_input_live.json").write_text(json.dumps(analyzer_input, ensure_ascii=False, indent=2), encoding="utf-8")
- print(json.dumps({
- "status": "ok",
- "videoCount": summary["videoCount"],
- "commentCount": summary["commentCount"],
- "effectiveKeywordCount": summary["effectiveKeywordCount"],
- "output": str(OUTPUT_DIR / "live-analysis-data.json"),
- "markdown": str(OUTPUT_DIR / "善存抖音稿件审核润色与评论VOC-live.md"),
- }, ensure_ascii=False, indent=2))
- if __name__ == "__main__":
- main()
|