初始提交:抖音视频批量采集流水线

App 端免签名接口(设备注册 + detail + CDN 直链),一体化流水线
下载→抽音→传COS→记录→删本地,AI 作品四档判定,四份文档:
README(使用指南)、PROGRESS(批次进度)、RE(逆向全过程)、AI(判定规则)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-29 16:31:10 +08:00
commit 2bb83f3685
39 changed files with 298874 additions and 0 deletions

276
tests/test_aiclass.py Normal file
View File

@@ -0,0 +1,276 @@
"""douyin.aiclass 的离线测试。用例全部取自 40087 条真实数据里的真实文案。"""
from __future__ import annotations
# 移到 scripts/ tests/ 后,直接运行时脚本目录成了 sys.path[0],
# 把项目根加回来,import douyin / pipeline 才找得到。
import sys as _sys
from pathlib import Path as _Path
_sys.path.insert(0, str(_Path(__file__).resolve().parent.parent))
from douyin.aiclass import classify
def rec(desc="", tags=(), author="", music="", is_aigc=False, label=""):
return {
"desc": desc,
"hashtags": [{"name": t} for t in tags],
"author": {"nickname": author},
"music": {"title": music},
"ai": {"is_aigc": is_aigc, "label": label},
}
def tier(**kw):
return classify(rec(**kw))["tier"]
# ---------- platform:唯一能抓到"作者没标但确实是 AI"的信号 ----------
def test_platform_label_wins_even_with_no_ai_text():
"""实测 1744 条:平台标了 AIGC,文案标签里一个 ai 字都没有。"""
assert tier(desc="#趣味脑筋急转弯 #创作者中心 #创作灵感",
tags=["趣味脑筋急转弯", "创作者中心", "创作灵感"],
author="申哥爱剪辑", is_aigc=True,
label="作者声明:内容由 AI 生成") == "platform"
assert tier(desc="澳洲的野狗 #中华田园犬 #历史", tags=["中华田园犬", "历史"],
author="胖嘟嘟历史", is_aigc=True) == "platform"
# ---------- strong:作品自己声明是 AI 做的 ----------
def test_strong_ai_compound():
assert tier(desc="AI花絮:我很快就会驯服AI演员啦 #AI短剧#AI电影",
tags=["AI短剧", "AI电影"]) == "strong"
def test_strong_tool_name_without_ai_letters():
"""即梦 / seedance / libtv 这些名字里没有 ai,只能靠名单。"""
assert tier(desc="干啥呀即梦,这是掀桌子不玩了吗?", tags=["即梦"]) == "strong"
assert tier(desc="", tags=["seedance2.0"]) == "strong"
assert tier(desc="本视频由libtv制作生成", tags=["Libtv"]) == "strong"
assert tier(desc="", tags=["runninghub"]) == "strong"
def test_strong_aigc_token():
assert tier(desc="自制AIGC《银骸》第0集!") == "strong"
def test_strong_handles_capital_A_lowercase_L():
"""`抖音al` 在这批数据里出现 129 次,`#Al创作` 也有。不归一化就整片漏掉。"""
assert tier(desc="他种下几千棵橡树 #Al创作 #爱情", tags=["Al创作", "爱情"]) == "strong"
assert tier(desc="", tags=["抖音al"]) == "topic" # 没有创作语境词,降到 topic
# ---------- topic:在讲 AI,不一定是 AI 做的 ----------
def test_topic_pure_ai_mention():
assert tier(desc="布布讲ai之如何写提示词 #ai", tags=["ai"]) == "strong" # 提示词是创作语境
assert tier(desc="今天聊聊 AI 会不会取代人类 #ai", tags=["ai"]) == "topic"
def test_topic_chat_models():
assert tier(desc="deepseek 又更新了") == "topic"
assert tier(desc="用 ChatGPT 写周报") == "topic"
assert tier(desc="#人工智能 的一点思考", tags=["人工智能"]) == "topic"
# ---------- author:最弱,作品本身没信号 ----------
def test_author_only():
r = classify(rec(desc="研究生偷吃外卖被毒S #湘潭大学 #普法教育",
tags=["湘潭大学", "普法教育"], author="牛牛Ai"))
assert r["tier"] == "author", r
def test_author_never_upgrades_a_scored_item():
"""作者昵称只能给 none 兜底,不能把 topic 顶成 strong。"""
r = classify(rec(desc="聊聊 AI 的伦理问题", author="星幕AIGC"))
assert r["tier"] == "topic", r
def test_default_original_sound_is_not_a_work_signal():
"""音乐标题默认是 `@<昵称>创作的原声`,40087 条里有 32555 条是这个壳。
昵称带 AI 会连"创作"一起命中,曾让 771 条从 music 这条路混进 strong
(@冠宇Aigc 的猴子炒菜、@牛牛Ai 的大案纪实)。只能落到 author 档。"""
r = classify(rec(desc="#猴子炒菜#猴子成精#原创视频",
tags=["猴子炒菜", "猴子成精"],
author="冠宇Aigc(AI剧场)",
music="@冠宇Aigc(AI剧场)创作的原声"))
assert r["tier"] == "author", r
# 作者昵称也不带 AI 时,默认原声不能凭空造出信号
assert tier(desc="今天天气不错", author="老王",
music="@老王创作的原声") == "none"
def test_real_track_name_still_counts_but_only_weakly():
assert tier(desc="随便发发", music="即梦AI生成的配乐") == "author"
# ---------- 误报:这些绝不能收 ----------
def test_english_words_containing_ai_are_not_signals():
for d in ["email me for detail", "again and again", "hair salon",
"wait a minute", "chairman", "trained model"]:
assert tier(desc=d) == "none", d
def test_editing_jargon_is_not_ai():
"""剪映 375 / 关键帧 314 / 蒙版 203 —— 剪辑术语,不是 AI。"""
assert tier(desc="导入一张照片即可制作 #剪映", tags=["剪映"]) == "none"
assert tier(desc="#关键帧 #蒙版 #转场 #剪辑教程",
tags=["关键帧", "蒙版", "转场", "剪辑教程"]) == "none"
def test_generic_chinese_words_alone_are_not_ai():
"""动画/绘画/分身/动漫 单独出现不算 —— 否则 830 条 #原创动画 全被误收。"""
assert tier(desc="第33 #原创动画", tags=["原创动画"]) == "none"
assert tier(desc="跟小同桌回老家 #二次元 #原创动画 #恋爱番",
tags=["二次元", "原创动画", "恋爱番"]) == "none"
assert tier(desc="他的分身术真牛") == "none"
def test_ordinary_content_is_none():
assert tier(desc="户外露营过夜用的这款帐篷也太实用啦! #露营装备",
tags=["露营装备"], author="蚁巢户外直营店") == "none"
assert tier(desc="4月16日,重庆,张雪陪同合作伙伴参观工厂 #张雪机车",
tags=["张雪机车"], author="第一现场") == "none"
def test_signals_are_recorded_for_audit():
r = classify(rec(desc="#即梦AI #AI短剧", tags=["即梦ai", "ai短剧"]))
assert r["tier"] == "strong"
assert any("工具:即梦" in s for s in r["signals"]), r["signals"]
def test_empty_record():
assert classify(rec())["tier"] == "none"
# ---------- 常用词工具名:必须跟 ai 同现,见 aiclass 模块开头第 6 条 ----------
def test_ambiguous_tool_alone_is_not_ai():
"""87110 条里实测的误判,全部取自真实文案。"""
# 三峰的"海螺"帐篷,撞上 MiniMax 的海螺 AI
assert tier(desc="三峰出-海螺。重量超级轻的一款帐篷,有tpu,有烟囱口。",
tags=["户外装备", "帐篷", "露营"]) == "none"
# 元宝勺,撞上腾讯元宝
assert tier(desc="边缘光滑才是好勺子!316不锈钢元宝勺。",
tags=["辅食勺", "勺子", "元宝勺"]) == "none"
# 豆包是只狗 / 一种包子,不是那个大模型
assert tier(desc="豆包监督学生自习这一块 豆包:交给我你就闹心吧!",
tags=["豆包", "搞笑", "晚自习"]) == "none"
# 《龙神万相》,撞上阿里万相
assert tier(desc="龙神万相之龙魂定档 迫不及待想沉浸式感受龙族的热血与传奇了!",
tags=["龙神万相之龙魂", "西行纪"]) == "none"
# "明星流量"里含"星流";"起舞弄清影"里含"清影"
assert tier(desc="商家别乱蹭明星流量!4 名被告擅自使用张雨绮肖像及姓名商用",
tags=["张雨绮", "侵权"]) == "none"
assert tier(desc="柳下闻瑶琴起舞弄清影 ~好久没跳了") == "none"
assert tier(desc="做白日梦从拥有弹丝云床开始",
tags=["户外露营", "自动充气床"]) == "none"
def test_ambiguous_tool_counts_when_ai_present():
"""真命中的无一例外都同时带 AI 字样,所以这道门槛不掉召回。"""
r = classify(rec(desc="海螺AI生成的短片 #ai视频", tags=["ai视频"]))
assert r["tier"] == "strong"
assert any("工具:海螺" in s for s in r["signals"]), r["signals"]
assert tier(desc="用豆包写的脚本,AI 生成 #ai", tags=["ai"]) == "strong"
def test_digital_rmb_is_not_a_digital_human():
"""数字人民币含"数字人",微信分身含"分身",穿搭也说"换脸"。
实测放开这三个词只进误判,一条真的都捞不到。"""
assert tier(desc="一地宣布:5月起,公务员工资用数字人民币发放",
tags=["公务员", "工资", "数字人民币"]) == "none"
assert tier(desc="抖音双开 小米华为苹果手机 微信怎么多开",
tags=["微信分身双开", "抖音多开分身"]) == "none"
assert tier(desc="救命!这套穿搭真的能换脸!",
tags=["今天穿什么", "冬季穿搭"]) == "none"
def test_sound_wrapper_csv_form_is_stripped():
"""csv 导出的原声标题多一截 ` - 昵称`,也得剥掉,
否则作者昵称又会从 music 这条路偷偷升档。"""
assert tier(desc="今天去钓鱼", music="@牛牛Ai创作的原声 - 牛牛Ai") == "none"
assert tier(desc="今天去钓鱼", music="@牛牛Ai创作的原声") == "none"
def test_minimax_is_a_tool():
assert tier(desc="minimax 生成的片子", tags=["minimax"]) == "strong"
def test_roman_numeral_impersonates_ai():
"""`AⅠ漫剧` 用罗马数字 Ⅰ(U+2160) 冒充字母 I,跟 `抖音al` 同一类花招。"""
r = classify(rec(desc="AⅠ漫剧你就这样崛起吧!", tags=["AⅠ漫剧", "漫剧推荐"]))
assert r["tier"] == "strong", r
def test_voice_cloning_is_generative():
assert tier(desc="克隆音色怎么弄 声音可以克隆吗",
tags=["克隆音色", "声音克隆", "剪映教程"]) == "strong"
def test_veo3_beats_a_weak_author_signal():
"""原先这批只靠作者昵称算 author 档,认出 veo3 后该升到 strong。"""
assert tier(desc="#猴子成精 #猴子炒菜 #veo3", tags=["猴子炒菜", "veo3"],
author="冠宇Aigc") == "strong"
def test_rejected_candidates_stay_none():
"""这些词看着像 AI,实测全是子串陷阱,加进词表会污染结果。"""
assert tier(desc="全国生猪价格已连续16周下跌,行业亏损幅度加大") == "none" # 度加
assert tier(desc="女生相亲要求要30万彩礼,听到要求男士都拒绝") == "none" # 万彩
assert tier(desc="中国男篮硬刚美国梦八队,诞生数副名场面!") == "none" # 生数
assert tier(desc="决定全年经济走势关键节点上,国务院召开重要会议") == "none" # 节点
assert tier(desc="卡池都快结束了,肝了个大保底怎么还不出货",
tags=["抽卡日常", "崩坏星穹铁道"]) == "none" # 抽卡
assert tier(desc="圈外人不语,只是一味求捏脸", tags=["燕云十六声捏脸"]) == "none" # 捏脸
assert tier(desc="杀神降临!82-36!全力帕到底有多恐怖?",
tags=["无畏契约", "valorant"]) == "none" # lora
assert tier(desc="剑来!!!", tags=["国产动漫", "国漫超燃", "剑来动画"]) == "none" # 国漫
def test_manju_names_stand_alone():
"""漫剧专名单独成立,不要求跟 ai 同现(用户要求:出现关键词就算)。"""
r = classify(rec(desc="究竟是什么样的小说仅是漫改,就能瞬间火爆全网破30亿的热度",
tags=["万妖图录传", "二次元", "充能计划"]))
assert r["tier"] == "strong", r
assert any("漫剧:万妖图录传" in x for x in r["signals"]), r["signals"]
assert tier(desc="晚夏小梦1-27:128分钟大合集看爽",
tags=["二次元", "原创动画", "晚夏小梦"]) == "strong"
assert tier(desc="小溪废片合集~", tags=["二次元", "月落小溪"]) == "strong"
def test_hongguo_platform_name_is_not_an_ai_signal():
"""红果主体是真人短剧,AI 漫剧只是其中一个赛道,平台名不能当信号。"""
assert tier(desc="把生活的爽感拉满!看《家里家外2》要备好纸巾",
tags=["红果短剧", "短剧推荐", "家里家外2", "治愈系短剧"]) == "none"
assert tier(desc="《冷御失守》后续", tags=["红果短剧"]) == "none"
def test_manju_topic_words_are_not_ai_markers():
"""漫剧圈里 100% 是 AI 的题材词,放全库就炸 —— 它们是题材不是 AI 标记。"""
assert tier(desc="玄奘故里,也就是西游记里唐僧的出生地,位于偃师缑氏镇") == "none"
assert tier(desc="宫崎骏的夏天大概就是这个样子吧!", tags=["宫崎骏的夏天", "龙猫"]) == "none"
assert tier(desc="《狼来了》第一集:还记得小时候听的《狼来了》吗?",
tags=["狼来了", "民间故事", "原创"]) == "none"
def main() -> int:
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
failed = 0
for t in tests:
try:
t()
print(f" ok {t.__name__}")
except AssertionError as exc:
failed += 1
print(f" FAIL {t.__name__}: {exc}")
print(f"\n{len(tests) - failed}/{len(tests)} 通过")
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())