App 端免签名接口(设备注册 + detail + CDN 直链),一体化流水线 下载→抽音→传COS→记录→删本地,AI 作品四档判定,四份文档: README(使用指南)、PROGRESS(批次进度)、RE(逆向全过程)、AI(判定规则)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
133 lines
5.7 KiB
Markdown
133 lines
5.7 KiB
Markdown
# 抖音采集流水线一体化 —— 设计
|
||
|
||
日期: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` 既有格式
|
||
- 不支持图文作品
|