Files
douyin_video_download/docs/superpowers/specs/2026-08-25-download-pipeline-design.md
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

133 lines
5.7 KiB
Markdown
Raw Permalink 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.

# 抖音采集流水线一体化 —— 设计
日期:2026-08-25
## 目标
把现在的 4 个独立阶段(下载 → 抽 mp3 → 探时长 → 传 COS)合成一条流水线,
每条视频端到端走完就删本地文件,并把标题等全部元信息记进一个本地 jsonl。
## 动机
1. **磁盘**:分阶段跑必须把全部 mp4 摊在盘上(本次 53 GB),单条流水线的
峰值只有 `workers × (mp4+mp3)`,约 100 MB,且与总量无关。
2. **元信息丢失**:现在只有 `.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` 在最前:
```json
{"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` 既有格式
- 不支持图文作品