App 端免签名接口(设备注册 + detail + CDN 直链),一体化流水线 下载→抽音→传COS→记录→删本地,AI 作品四档判定,四份文档: README(使用指南)、PROGRESS(批次进度)、RE(逆向全过程)、AI(判定规则)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
"""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
|