初始提交:抖音视频批量采集流水线

App 端免签名接口(设备注册 + detail + CDN 直链),一体化流水线
下载→抽音→传COS→记录→删本地,AI 作品四档判定,四份文档:
README(使用指南)、PROGRESS(批次进度)、RE(逆向全过程)、AI(判定规则)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-29 16:31:10 +08:00
commit 2bb83f3685
39 changed files with 298874 additions and 0 deletions

71
douyin/audio.py Normal file
View File

@@ -0,0 +1,71 @@
"""ffmpeg / ffprobe 的薄封装:找可执行文件、拼抽音命令、探时长。
批量抽取的调度不在这里 —— `pipeline.py` 自己按条编排(下载→抽音→上传→删),
所以这个模块只提供单次调用要用的零件。
从 mp4 里提取音轨。
源音频是 AAC 44.1kHz 立体声 128kbps,据此有两种模式:
mp3 转码成 MP3。有损转有损,必然掉质量。码率给太高只是浪费空间
(源就 128k),默认 192k 是留余量避免二次编码劣化,不是"更清晰"。
copy 直接把 AAC 流拷进 .m4a 容器。无损、秒完、体积更小。
只有在必须是 .mp3 后缀时才用 mp3 模式。
依赖 ffmpeg,需在 PATH 里。
"""
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
from typing import List, Optional
class FFmpegMissing(RuntimeError):
pass
def ensure_ffmpeg(exe: str = "ffmpeg") -> str:
path = shutil.which(exe)
if not path:
raise FFmpegMissing(
f"找不到 {exe}。装一个并加进 PATH,或用 --ffmpeg 指定完整路径。")
return path
def build_cmd(ffmpeg: str, src: Path, dst: Path, mode: str, bitrate: str) -> List[str]:
cmd = [ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-i", str(src), "-vn"]
if mode == "copy":
# 不重新编码,直接搬运 AAC 流
cmd += ["-acodec", "copy"]
else:
cmd += ["-acodec", "libmp3lame", "-b:a", bitrate]
cmd.append(str(dst))
return cmd
def probe(path, ffprobe: str = "ffprobe", timeout: float = 60.0):
"""探一个媒体文件的容器时长,探不出来返回 None。
时长取自 mp4(源),不取 mp3 —— mp3 是转码产物,虽然实测两者一致,
但源文件才是权威。
注意这里读的是容器声明的 format=duration,对 mp4 没问题,
但**不能拿去判断 mp3 有没有被截断**:LAME 写的 Xing 头声明的是原始
总帧数,ffprobe 直接信它而不解码,实测把 mp3 截到 20% 它报的还是原时长。
那种场景要用 pipeline.decoded_seconds()(-count_frames 真解码)。
"""
cmd = [ffprobe, "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", str(path)]
try:
p_ = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
except subprocess.TimeoutExpired:
return None
try:
d = float((p_.stdout or "").strip())
except ValueError:
return None
return d if d > 0 else None