"""FFmpeg 提音节点入口。 通过标准节点 HTTP 服务对外提供音频提取能力。ffmpeg 解析顺序为: FFMPEG_BIN 环境变量 > PATH 中的 ffmpeg > imageio-ffmpeg 内置二进制。 """ from __future__ import annotations import json import os import shutil import subprocess from pathlib import Path from wov_sdk.models import InvokeRequest, InvokeResponse, NodeManifest from wov_sdk.server import run_node def _bundled_ffmpeg() -> str | None: """尝试获取 imageio-ffmpeg 内置的 ffmpeg 可执行文件路径。""" try: import imageio_ffmpeg return imageio_ffmpeg.get_ffmpeg_exe() except Exception: # noqa: BLE001 # 未安装 imageio-ffmpeg 或获取失败时返回 None,交由上层回退。 return None def _ffmpeg_bin() -> str: """按优先级解析 ffmpeg 可执行文件,返回最终命令路径。""" # 显式配置优先,便于部署环境指定自定义二进制。 configured = os.getenv("FFMPEG_BIN") if configured: return configured # 其次查找 PATH 中的系统 ffmpeg。 found = shutil.which("ffmpeg") if found: return found # 最后回退到 imageio-ffmpeg 内置二进制;都没有时保留 "ffmpeg" 交给调用失败处理。 return _bundled_ffmpeg() or "ffmpeg" def invoke(request: InvokeRequest) -> InvokeResponse: """提取输入视频/音频的标准化音频,产物为 audio.wav。""" video_uri = request.inputs.get("video_uri") if not video_uri: return InvokeResponse(status="failed", error="video_uri is required") # 找不到可用 ffmpeg 时直接返回失败,避免子进程报晦涩错误。 ffmpeg = _ffmpeg_bin() if shutil.which(ffmpeg) is None and not Path(ffmpeg).is_file(): return InvokeResponse(status="failed", error="ffmpeg not found") output_dir = Path(request.output_dir) output_dir.mkdir(parents=True, exist_ok=True) output_path = output_dir / "audio.wav" # ASR 节点默认期望 16kHz 单声道;参数可覆盖。 channels = str(request.params.get("channels", 1)) sample_rate = str(request.params.get("sample_rate", 16000)) command = [ ffmpeg, "-y", # 覆盖可能存在的同名输出文件。 "-i", str(video_uri), "-vn", # 丢弃视频流,只保留音频。 "-ac", channels, "-ar", sample_rate, str(output_path), ] result = subprocess.run(command, capture_output=True, text=True) if result.returncode != 0: # 返回 stderr 尾部,保留最有诊断价值的错误信息。 return InvokeResponse( status="failed", error=result.stderr[-2000:] or "ffmpeg failed", ) return InvokeResponse(status="completed", outputs={"audio_uri": str(output_path)}) def main() -> None: """加载节点清单并以本模块的 invoke 处理器启动服务。""" manifest_path = Path(__file__).resolve().parent.parent / "node.manifest.json" with open(manifest_path, "r", encoding="utf-8") as f: manifest = NodeManifest.from_dict(json.load(f)) run_node(manifest, invoke) if __name__ == "__main__": main()