初始提交:抖音视频批量采集流水线
App 端免签名接口(设备注册 + detail + CDN 直链),一体化流水线 下载→抽音→传COS→记录→删本地,AI 作品四档判定,四份文档: README(使用指南)、PROGRESS(批次进度)、RE(逆向全过程)、AI(判定规则)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
258
douyin/aiclass.py
Normal file
258
douyin/aiclass.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""判定一条抖音作品是不是 AI 相关,以及是哪一类相关。
|
||||
|
||||
分四档,档位之间不重叠,取最强的那一档:
|
||||
|
||||
platform 抖音自己打了 AIGC 标(ai.is_aigc)。最可信,而且是唯一能抓到
|
||||
"作者什么都没标、但确实是 AI 生成"的信号 —— 实测 1744 条属于
|
||||
这种,文案标签里一个 ai 字都没有(脑筋急转弯、历史解说、原创动画)。
|
||||
strong 文案/标签里有明确的"AI 生成作品"信号:AIGC、AI 生成类工具名、
|
||||
ai+创作类词、平台的 AI 赛事标签。
|
||||
topic 只提到 AI 本身(ai 教程、人工智能、大模型、ChatGPT 之类),
|
||||
片子讲的是 AI,但不一定是 AI 做的。
|
||||
author 作品本身没信号,只有作者昵称里带 AI/AIGC。最弱,会误收
|
||||
(实测 @牛牛Ai 发的普法解说、@你好,我是大福 的真人游泳视频)。
|
||||
|
||||
坑(都是从这批 40087 条真实数据里挖出来的,别顺手删):
|
||||
|
||||
1. "Al"(大写 A + 小写 L)被当成 AI 用,标签 `抖音al` 出现 129 次,
|
||||
文案里也有 `#Al创作`。必须归一化,否则整片漏掉。
|
||||
2. 反过来,`ai` 是英文单词的常见片段(email / detail / again / hair),
|
||||
所以只认前后不接字母数字的独立 ai,不能用 in 判断。
|
||||
3. `剪映`/`关键帧`/`蒙版`/`转场`/`剪辑教程` 是剪辑术语,不是 AI。
|
||||
它们在这批数据里量很大(剪映 375、关键帧 314、蒙版 203),
|
||||
早期版本把"动画/绘画/分身"这类词单独当信号,误收了几千条。
|
||||
所以中文泛词一律要求跟 AI 词同现,不单独成立。
|
||||
4. 工具名不含 ai 字样的一大票:即梦 / seedance / libtv / flovaai /
|
||||
小云雀 / 随变 / runninghub / oiioii / shotlab,只能靠名单。
|
||||
5. 音乐标题默认是 `@<作者昵称>创作的原声`,40087 条里有 32555 条是这个壳。
|
||||
昵称带 AI 就会连"创作"一起命中,等于让作者昵称从 music 这条路
|
||||
偷偷升到 strong —— 实测 771 条这样混进来(@冠宇Aigc 的猴子炒菜、
|
||||
@牛牛Ai 的大案纪实)。所以先剥壳,剥完只当弱信号:全库剥完只剩
|
||||
16 条音乐标题还有命中,这条路本来就没什么价值。
|
||||
注意壳有两种写法:jsonl 里是 `@昵称创作的原声`,csv 导出的是
|
||||
`@昵称创作的原声 - 昵称`,后缀那半截也要一起剥。
|
||||
6. 一批工具名本身就是常用词,单独出现几乎全是误判,必须要求跟 ai/aigc
|
||||
同现(见 _AMBIG_*)。实测:豆包 224 条误判(狗名、豆沙包、"反强包
|
||||
之神")对 145 条真命中;海螺 13 条是三峰的"海螺"帐篷;元宝 11 条是
|
||||
元宝勺辅食餐具;万相 17 条是《龙神万相》和万相集;星流 1 条来自
|
||||
"明星流量"(子串);清影 1 条来自"起舞弄清影"。而真命中的那些**无一
|
||||
例外**都同时带 AI 字样,所以加这道门槛不掉召回。
|
||||
7. 别把"数字人""换脸""分身"放开成单独信号:数字人民币含"数字人"、
|
||||
微信分身/五五分身材含"分身"、穿搭视频也说"这套穿搭能换脸"。
|
||||
实测放开只进误判,一条真的都捞不到。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
# 独立的 ai —— 前后不能接字母数字,挡掉 email、detail、hair。
|
||||
# 第二个字符收了三种冒充写法:i、小写 L、罗马数字 Ⅰ/ⅰ(U+2160/U+2170)。
|
||||
# 实测标签 `抖音al` 129 次、`#AⅠ漫剧` 1 条。别改成简单的 in 判断。
|
||||
_I = r'[ilⅠⅰ]'
|
||||
_AI = re.compile(r'(?<![a-z0-9])a' + _I + r'(?![a-z0-9])', re.I)
|
||||
_AIGC = re.compile(r'(?<![a-z0-9])a' + _I + r'gc(?![a-z0-9])', re.I)
|
||||
|
||||
# 生成类工具 / 平台。命中即 strong —— 用了这些东西,片子就是 AI 做的。
|
||||
_GEN_TOOLS = [
|
||||
"即梦", "dreamina", "jimeng", "seedance", "seedream", "可灵", "kling",
|
||||
"hailuo", "vidu", "pixverse", "pika", "luma", "runway", "sora",
|
||||
"viggle", "heygen", "midjourney", "comfyui", "runninghub", "flux",
|
||||
"nanobanana", "nano banana", "stable diffusion", "liblib",
|
||||
"krea", "ideogram", "recraft", "leonardo",
|
||||
"higgsfield", "suno", "udio", "libtv", "flovaai", "海艺", "小云雀",
|
||||
"shotlab", "sekoai", "movieflow", "updream", "cineart", "oiioii",
|
||||
"minimax", "妙鸭", "一帧秒创", "触手ai", "拍我ai", "美图ai",
|
||||
"文生视频", "图生视频", "文生图", "图生图", "生成式",
|
||||
# --- 下面这批是 2026-08 补的。全部在 12.7 万条真实数据上验过:
|
||||
# 要么零命中(纯属给以后的文件备着),要么命中的全是真 AI,
|
||||
# 没有一个引入新误判。判断标准是"这串字在中文短视频文案里
|
||||
# 不可能有别的意思",所以像 tripo(三脚架 tripod)、seaweed
|
||||
# (海藻)、lora(撞 valorant)、captions、playground 这些
|
||||
# 一律没收 —— 实测 lora 命中 3 条全是无畏契约电竞。
|
||||
"veo3", "wan2.1", "wanx", "通义万相", "hunyuanvideo", "混元视频",
|
||||
"cogvideo", "open-sora", "animatediff", "controlnet", "智谱清影",
|
||||
"step-video", "hedra", "kaiber", "haiper", "moonvalley", "genmo",
|
||||
"domoai", "synthesia", "akool", "deepbrain", "opusclip", "argil",
|
||||
"kreadoai", "腾讯智影", "讯飞智作", "硅基智能", "魔音工坊", "元气口播",
|
||||
"万兴播爆", "闪剪", "声音克隆", "语音克隆", "克隆音色", "音色克隆",
|
||||
]
|
||||
|
||||
# AI 漫剧圈的专有名词:**单独出现即 strong**,不要求跟 ai 同现。
|
||||
# 抖音的 AI 漫剧已经形成固定圈子,这些词在 18.4 万条真实数据里几乎只出现在
|
||||
# AI 作品上(括号=全库命中/其中现在还漏掉的)。选词标准是"这串字在中文短视频
|
||||
# 文案里不可能有别的意思":
|
||||
# 漫剧(1568/64) 治愈系漫画(80/0) 渡灵书(62/0) 超级裂变(46/0) 漫剧推荐(38/2)
|
||||
# 二次元动漫(33/0) 月落小溪(25/4) 七罪创世(21/0) 晚夏小梦(67/4)
|
||||
# 灵绘漫客(12/0) neowow(12/0) 造梦国度(2/0) 猫箱跨次元相遇(114/12)
|
||||
# 没收的(实测污染严重,它们是题材不是 AI 标记):
|
||||
# 西游记(+26 真·玄奘故里游记) 孙悟空(+16) 宫崎骏(+10 龙猫童年) 民间故事(+7《狼来了》)
|
||||
# 睡前故事(+5 爷俩演西游) 神话故事(+2) 国漫推荐(+4《剑来》真人制作)
|
||||
# 原创动漫(+18 手绘二次元) 创作者激励计划(+12 原神/卡西尼号) 未来导演扶持计划(+4)
|
||||
# 开放赛道(+2) —— 后两个是抖音通用活动,不专属 AI
|
||||
_MANJU = [
|
||||
"漫剧", "治愈系漫画", "渡灵书", "超级裂变", "二次元动漫", "月落小溪",
|
||||
"七罪创世", "晚夏小梦", "灵绘漫客", "neowow", "造梦国度", "猫箱跨次元相遇",
|
||||
# 红果(字节的免费短剧 App)上的 AI 漫剧片名。来源是新闻报道的热榜,
|
||||
# **不是红果的完整目录** —— 红果不公开片库,AI 漫剧实际有成百上千部,
|
||||
# 这里只有见诸报道的头部作品,以后遇到新的往这加。
|
||||
# 在 18.4 万条上验过:16/18 零命中(纯属备着),万妖图录传 1 条且正是
|
||||
# 现在漏掉的真 AI 漫剧,零污染。
|
||||
"万妖图录传", "发配边关", "罪妻开荒养出战神", "儿媳的重生棋局",
|
||||
"斩仙台", "菩提临世", "聚宝仙盆", "杂灵根", "渔乡守真心", "金钱照冷暖",
|
||||
"杀猪辣媳", "夫人一个过肩摔", "瘫子总裁站起来", "凡人百世书",
|
||||
"都重生了谁还装富二代",
|
||||
# **"红果"/"红果短剧" 不能进这个表**:红果主体是真人短剧,AI 漫剧只是
|
||||
# 其中一个赛道。实测 3 条命中里 2 条是真人短剧(《家里家外2》《冷御失守》),
|
||||
# 1 条是短剧演员面试。要抓红果的 AI 漫剧只能靠具体片名。
|
||||
]
|
||||
|
||||
# 名字本身是常用词的生成类工具:只有跟 ai/aigc 同现才算 strong。
|
||||
# 见模块开头第 6 条 —— 单独出现基本全是误判,真命中都带 AI 字样。
|
||||
_AMBIG_GEN = [
|
||||
"海螺", "白日梦", "万相", "可图", "清影", "星流", "秒画",
|
||||
"造梦日记", "随变",
|
||||
]
|
||||
|
||||
# 对话/通用大模型。这些更像是"在聊 AI",归 topic。
|
||||
_CHAT_TOOLS = [
|
||||
"chatgpt", "deepseek", "kimi", "通义", "千问", "qwen", "文心",
|
||||
"ernie", "智谱", "glm", "copilot", "gemini", "grok",
|
||||
"人工智能", "大模型", "神经网络", "机器学习", "深度学习",
|
||||
]
|
||||
# 同上,但归 topic 档的那批常用词模型名
|
||||
_AMBIG_CHAT = ["豆包", "元宝", "混元"]
|
||||
# 单独成词才算,避免 gpt 撞上别的串
|
||||
_CHAT_RE = re.compile(r'(?<![a-z0-9])(?:gpt|claude|llm|agi|ai\s*agent)(?![a-z0-9])')
|
||||
|
||||
# 跟 ai 同现才成立的"创作/生成"语境词。单独出现一律不算。
|
||||
_GEN_CTX = [
|
||||
"短剧", "漫剧", "动画", "动漫", "视频", "短片", "电影", "影视", "绘画",
|
||||
"绘图", "创作", "制作", "生成", "分身", "剧情", "作画", "配音", "换脸",
|
||||
"特效", "导演", "演员", "数字人", "虚拟人", "成片", "新影像", "影像",
|
||||
"浪潮", "大赛", "赛道", "大赏", "工作台", "写真", "画师", "作品", "片子",
|
||||
"微短剧", "番剧", "剪辑", "转绘", "扩图", "修图", "提示词",
|
||||
]
|
||||
_GEN_CTX_RE = re.compile("|".join(map(re.escape, _GEN_CTX)))
|
||||
|
||||
def _alt(words: List[str]) -> re.Pattern:
|
||||
"""把词表编成一个正则。纯 ASCII 的词加字母边界:前后不能紧接字母,
|
||||
但**允许接数字和中文** —— 数据里满地都是 `Seedance2.0`、`sora2`、
|
||||
`runninghub0.21`、`libtv创作者`、`flovaai超创`,一刀切加 \\b 会全丢。
|
||||
只挡真正的词中词:`udio` 藏在 `theonestudio`/`Grass Studio` 里 17 次,
|
||||
`flux` 藏在 `Fluxxwave` 曲名里 1 次,两个都是从 music 混进 author 档的。
|
||||
"""
|
||||
parts = []
|
||||
for w in words:
|
||||
e = re.escape(w)
|
||||
if re.fullmatch(r"[a-z0-9 .\-]+", w):
|
||||
e = r"(?<![a-z])" + e + r"(?![a-z])"
|
||||
parts.append(e)
|
||||
return re.compile("|".join(parts))
|
||||
|
||||
|
||||
_TOOL_RE = _alt(_GEN_TOOLS)
|
||||
_CHATW_RE = _alt(_CHAT_TOOLS)
|
||||
_MANJU_RE = _alt(_MANJU)
|
||||
_AMBIG_GEN_RE = _alt(_AMBIG_GEN)
|
||||
_AMBIG_CHAT_RE = _alt(_AMBIG_CHAT)
|
||||
|
||||
# 抖音默认原声的标题壳,里面就是作者昵称,不含额外信息。
|
||||
# jsonl 是 `@昵称创作的原声`,csv 导出多一截 ` - 昵称`,见模块开头第 5 条。
|
||||
_SOUND_WRAP = re.compile(r'^@.*?(?:创作的原声|的原声)(?:\s*-\s*.*)?$')
|
||||
|
||||
TIERS = ("platform", "strong", "topic", "author", "none")
|
||||
_RANK = {t: i for i, t in enumerate(TIERS)}
|
||||
|
||||
|
||||
def _fields(rec: Dict[str, Any]) -> Tuple[str, str, str]:
|
||||
"""(作品文本, 作者昵称, 音乐标题)。作品文本 = desc + 所有标签名。"""
|
||||
tags = [t.get("name", "") if isinstance(t, dict) else str(t)
|
||||
for t in (rec.get("hashtags") or [])]
|
||||
body = " ".join([rec.get("desc") or ""] + tags)
|
||||
author = (rec.get("author") or {}).get("nickname") or ""
|
||||
music = ((rec.get("music") or {}).get("title") or "").strip()
|
||||
if _SOUND_WRAP.match(music):
|
||||
music = "" # 只是作者昵称的复读,见模块开头第 5 条
|
||||
return body, author, music
|
||||
|
||||
|
||||
def _scan(text: str, where: str) -> Tuple[str, List[str]]:
|
||||
"""扫一段文本,返回 (档位, 命中的信号)。档位只可能是 strong/topic/none。"""
|
||||
low = text.lower()
|
||||
sig: List[str] = []
|
||||
tier = "none"
|
||||
|
||||
has_aigc = bool(_AIGC.search(low))
|
||||
has_ai = bool(_AI.search(low))
|
||||
# 歧义词表的开关:文本里得先有 ai/aigc 字样,那些常用词才作数
|
||||
ai_present = has_ai or has_aigc
|
||||
|
||||
if has_aigc:
|
||||
sig.append(f"{where}:aigc")
|
||||
tier = "strong"
|
||||
|
||||
for m in sorted(set(_TOOL_RE.findall(low))):
|
||||
sig.append(f"{where}:工具:{m}")
|
||||
tier = "strong"
|
||||
|
||||
# 漫剧专名单独成立,不要求 ai 同现
|
||||
for m in sorted(set(_MANJU_RE.findall(low))):
|
||||
sig.append(f"{where}:漫剧:{m}")
|
||||
tier = "strong"
|
||||
|
||||
if ai_present:
|
||||
for m in sorted(set(_AMBIG_GEN_RE.findall(low))):
|
||||
sig.append(f"{where}:工具:{m}")
|
||||
tier = "strong"
|
||||
|
||||
if has_ai:
|
||||
ctx = sorted(set(_GEN_CTX_RE.findall(text)))
|
||||
if ctx:
|
||||
sig.append(f"{where}:ai+{'/'.join(ctx[:4])}")
|
||||
tier = "strong"
|
||||
else:
|
||||
sig.append(f"{where}:ai")
|
||||
if tier == "none":
|
||||
tier = "topic"
|
||||
|
||||
chat = set(_CHATW_RE.findall(low)) | set(_CHAT_RE.findall(low))
|
||||
if ai_present:
|
||||
chat |= set(_AMBIG_CHAT_RE.findall(low))
|
||||
chat = sorted(chat)
|
||||
if chat:
|
||||
sig.append(f"{where}:模型:{'/'.join(chat[:4])}")
|
||||
if tier == "none":
|
||||
tier = "topic"
|
||||
|
||||
return tier, sig
|
||||
|
||||
|
||||
def classify(rec: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""判一条记录。返回 {tier, signals, is_aigc}。"""
|
||||
body, author, music = _fields(rec)
|
||||
signals: List[str] = []
|
||||
tier = "none"
|
||||
|
||||
is_aigc = bool((rec.get("ai") or {}).get("is_aigc"))
|
||||
label = ((rec.get("ai") or {}).get("label") or "").strip()
|
||||
if is_aigc:
|
||||
tier = "platform"
|
||||
signals.append(f"platform:{label or 'is_aigc'}")
|
||||
|
||||
t, s = _scan(body, "desc")
|
||||
signals.extend(s)
|
||||
if _RANK[t] < _RANK[tier]:
|
||||
tier = t
|
||||
|
||||
# 作者昵称和音乐标题都只是"谁发的",不是"作品是什么"。
|
||||
# 只在作品自己毫无信号时兜底成 author 档,绝不往上顶。
|
||||
for where, text in (("author", author), ("music", music)):
|
||||
wt, wsig = _scan(text, where)
|
||||
if wt != "none":
|
||||
signals.extend(wsig)
|
||||
if tier == "none":
|
||||
tier = "author"
|
||||
|
||||
return {"tier": tier, "signals": signals, "is_aigc": is_aigc}
|
||||
Reference in New Issue
Block a user