Files
douyin_video_download/tests/test_aiclass.py
whale-fall-code 2bb83f3685 初始提交:抖音视频批量采集流水线
App 端免签名接口(设备注册 + detail + CDN 直链),一体化流水线
下载→抽音→传COS→记录→删本地,AI 作品四档判定,四份文档:
README(使用指南)、PROGRESS(批次进度)、RE(逆向全过程)、AI(判定规则)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 16:31:10 +08:00

277 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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())