App 端免签名接口(设备注册 + detail + CDN 直链),一体化流水线 下载→抽音→传COS→记录→删本地,AI 作品四档判定,四份文档: README(使用指南)、PROGRESS(批次进度)、RE(逆向全过程)、AI(判定规则)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
5.7 KiB
抖音采集流水线一体化 —— 设计
日期:2026-08-25
目标
把现在的 4 个独立阶段(下载 → 抽 mp3 → 探时长 → 传 COS)合成一条流水线, 每条视频端到端走完就删本地文件,并把标题等全部元信息记进一个本地 jsonl。
动机
- 磁盘:分阶段跑必须把全部 mp4 摊在盘上(本次 53 GB),单条流水线的
峰值只有
workers × (mp4+mp3),约 100 MB,且与总量无关。 - 元信息丢失:现在只有
.downloaded.json记了 author/desc/uri,且它跟着 输出目录一起被删掉了。元信息需要一个独立于媒体文件的落点。
架构
一个 worker 端到端负责一条,不再分阶段:
1. fetch_detail(id) → Aweme + raw
2. 下载 mp4 → {--work-dir}/{id}.mp4
3. ffmpeg 抽 mp3 → {--work-dir}/{id}.mp3 [信号量限流]
4. ffprobe 探 mp4 时长;mp3 用 -count_frames 实际解码算时长
5. 校验 |mp3实解时长 - mp4时长| <= max(1s, 2%) ← 抽取闸门
6. 上传 mp4 → COS(覆盖同名对象)
7. 上传 mp3 → COS(覆盖同名对象)
8. head_object 核对两个对象 Content-Length == 本地字节数 ← 删除闸门
9. 追加 record 到 records.jsonl,然后删本地 mp4 / mp3
工作目录由 --work-dir 指定,默认 work/,mp4 和 mp3 同放一处——
反正传完就删,不需要像原来那样分 downloads/ 和 audio/。
并发
- 下载、上传是网络密集 → 按
--workers并发(默认 8) - ffmpeg 是 CPU 密集 → 单独用信号量限到
cpu_count,避免 8 个 worker 同时撞进第 3 步把 CPU 打满
失败处理
任何一步失败:保留本地文件不删,写入 pipeline_failed.txt(id + 阶段 + 原因),
继续下一条。不中断整批。
断点续跑
启动时扫 records.jsonl 已完成的 id。为避免对数百 MB 的文件逐行 json.loads,
aweme_id 必须是每行的第一个字段,扫描用 ^\{"aweme_id":"(\d+)" 直接抠。
这是一个契约,重排字段会让启动扫描从 ~1s 退化到十几秒。
数据
records.jsonl(唯一落点,仅本地,不上传 COS)
每行一条,字段顺序固定,aweme_id 在最前:
{"aweme_id":"6797367856917990659",
"title":"剥掉标签后的纯标题",
"desc":"服务端原样 desc,一字不改",
"tags":["标签1","标签2"],
"author":{"nickname":"...","uid":"...","sec_uid":"..."},
"create_time":1582635538,
"create_time_str":"2020-02-25 21:38:58",
"duration_sec":41.702993,
"duration_ms_api":41750,
"stats":{"digg":0,"comment":0,"collect":0,"share":0,"play":0},
"music":{"title":"...","author":"...","url":"..."},
"cover_url":"...",
"video":{"uri":"...","width":1280,"height":720},
"cos_mp4":"cos://asr-dig-data-1253181912/0005/zh/{id}.mp4",
"cos_mp3":"cos://asr-dig-data-1253181912/0005/zh/{id}.mp3",
"size_mp4":4026560,
"size_mp3":1001230,
"source_url":"https://www.douyin.com/video/{id}",
"fetched_at":1756000000,
"raw":{ ...服务端 aweme_detail 原样... }}
duration_sec(ffprobe 精确解码值)与 duration_ms_api(服务端声明值)都留:
实测同一条 41703 ms vs 41750 ms,不等价,前者才能用于 ASR 切分。
预计体积 200~400 MB / 5400 条。因此所有读取必须逐行流式。
标题拆分
desc—— 原样照抄,唯一真相来源title—— 用text_extra的 start/end 偏移剥掉标签,再用正则兜底tags—— 正则从desc提取全集,text_extra只用来补 hashtag_id
理由:实测 12 条样本中有 2 条的标签在 desc 里有、text_extra 里没有
(如 #非洲搞笑视频、#女人)。以 text_extra 为准会静默丢标签。
文件改动
| 文件 | 改动 |
|---|---|
pipeline.py |
新增,流水线入口 |
douyin/meta.py |
新增:split_desc() / build_record() / RecordLog |
douyin/cos.py |
新增:从 upload_cos.py 抽出 CosUploader(含 verify()) |
douyin/client.py |
Aweme 补 tags / duration_ms / uid / sec_uid / stats / width / height |
jsonl_to_xlsx.py |
改流式读取,支持 records.jsonl,加 --columns |
test_smoke.py |
加 split_desc / RecordLog 离线测试 |
原有 4 个单阶段脚本保留不动,仍可单独使用。
安全
- 删除是唯一不可逆操作,卡在第 5、8 两道闸门之后
- mp3 截断校验必须实际解码。
ffprobe format=duration读的是 LAME 写的 Xing 头,它声明的是原始总帧数:实测把 mp3 截断到 20%,报的仍然是完整的 10.0078s,和没截断时一字不差。ffmpeg -f null -也拦不住(rc=0、无 stderr)。 只有-count_frames会真解码,代价是 120s 的 mp3 多花 0.056s(0.109 vs 0.053) --keep-local关闭删除,供试跑- COS 上已存在同名对象时直接覆盖,不做存在性检查、不比对远端大小。 本地这一份永远是权威版本。少一个分支,行为也更好预测。
- 重复劳动由
records.jsonl的断点续跑拦掉(已完成的 id 整条跳过, 连下载都不会发生),所以不需要再靠 COS 存在性去省事 - COS 凭证只从环境变量
COS_SECRET_ID/COS_SECRET_KEY读
重跑老数据的后果
拿原来那 5406 条链接重跑,会重新下载约 53 GB 并覆盖 COS 上现有的 68 GB。 这是预期行为——覆盖后本地与 COS 一致,且拿到全量元信息。
不做
- 不回填已下载的 5393 条(除非显式拿老链接重跑)
- 不改
manifest.jsonl/抖音-四川话.xlsx既有格式 - 不支持图文作品