Files
douyin_video_download/douyin/audio.py
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

72 lines
2.6 KiB
Python
Raw 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.

"""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