From 58422aeda4d63b7b93898cba159ba839f8f4b7cb Mon Sep 17 00:00:00 2001 From: cat-shark Date: Thu, 13 Aug 2026 22:09:55 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=B8=BA=E5=85=A8=E9=83=A8=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E8=A1=A5=E5=85=85=E4=B8=AD=E6=96=87=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E5=B9=B6=E5=8A=A0=E5=85=A5=20AGENTS=20=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 7 +++++++ pyproject.toml | 5 +++++ tests/test_ffmpeg_node.py | 14 ++++++++++++++ wov_node_ffmpeg/__init__.py | 6 +++++- wov_node_ffmpeg/__main__.py | 21 +++++++++++++++++++-- 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1fcd4ef..2d2f255 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,3 +16,10 @@ - Windows 和 Linux 均支持。 - 优先使用 `FFMPEG_BIN`,其次 PATH 中的 `ffmpeg`,最后使用 `imageio-ffmpeg` 内置二进制。 - 不写死 ffmpeg 路径。 + +## 代码注释规范 + +- 本仓库所有源码(Python、TOML 等支持注释的文件)必须配有详细中文注释,说明模块职责、ffmpeg 解析与调用逻辑,确保后续维护人员可以快速理解代码工作原理。 +- 新增或修改代码时,必须同步补充或更新对应注释;不得删除已有注释。 +- 测试代码同样必须配有中文注释,说明每条测试验证的行为。 +- JSON 数据文件(`node.manifest.json`)不支持注释,字段语义以 `wov-sdk` 的 `NodeManifest` 模型注释和本文档输入/输出说明为准。 diff --git a/pyproject.toml b/pyproject.toml index e67683b..d5fce29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,4 @@ +# WOV FFmpeg 节点配置:使用 uv 管理环境与依赖。 [project] name = "wov-node-ffmpeg" version = "0.1.0" @@ -5,16 +6,20 @@ description = "WOV ffmpeg media node" requires-python = ">=3.11" dependencies = ["wov-sdk", "imageio-ffmpeg>=0.6"] +# 本地路径依赖 wov-sdk;imageio-ffmpeg 用于内置 ffmpeg 回退。 [tool.uv.sources] wov-sdk = { path = "../wov-sdk" } +# 开发依赖:pytest 与覆盖率工具。 [dependency-groups] dev = ["pytest", "pytest-cov"] +# pytest 配置:强制 100% 行覆盖率。 [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] addopts = "--cov=wov_node_ffmpeg --cov-report=term-missing --cov-fail-under=100" +# 仅打包节点包本身。 [tool.setuptools] packages = ["wov_node_ffmpeg"] diff --git a/tests/test_ffmpeg_node.py b/tests/test_ffmpeg_node.py index 386a1a3..eec03e9 100644 --- a/tests/test_ffmpeg_node.py +++ b/tests/test_ffmpeg_node.py @@ -1,3 +1,8 @@ +"""FFmpeg 提音节点测试。 + +覆盖 ffmpeg 解析优先级、成功/失败执行、缺失输入与入口点启动等真实路径。 +""" + import json import runpy import subprocess @@ -9,6 +14,7 @@ from wov_sdk.models import InvokeRequest def _request(tmp_path, **overrides) -> InvokeRequest: + """构造包含默认视频输入与提音参数的调用请求。""" payload = { "run_id": "run_1", "node_instance_id": "ni_1", @@ -21,6 +27,7 @@ def _request(tmp_path, **overrides) -> InvokeRequest: def test_success(tmp_path, monkeypatch) -> None: + """验证成功调用会生成 audio.wav 产物。""" def fake_run(command, **kwargs): output = Path(command[-1]) output.parent.mkdir(parents=True, exist_ok=True) @@ -35,6 +42,7 @@ def test_success(tmp_path, monkeypatch) -> None: def test_configured_ffmpeg(tmp_path, monkeypatch) -> None: + """验证 FFMPEG_BIN 环境变量优先于 PATH 查找。""" fake_bin = tmp_path / "ffmpeg.exe" fake_bin.write_bytes(b"") monkeypatch.setenv("FFMPEG_BIN", str(fake_bin)) @@ -44,6 +52,7 @@ def test_configured_ffmpeg(tmp_path, monkeypatch) -> None: def test_bundled_ffmpeg_fallback(tmp_path, monkeypatch) -> None: + """验证无系统 ffmpeg 时回退到 imageio-ffmpeg 内置二进制。""" monkeypatch.delenv("FFMPEG_BIN", raising=False) monkeypatch.setattr("wov_node_ffmpeg.__main__.shutil.which", lambda _: None) bundled = _ffmpeg_bin() @@ -62,6 +71,7 @@ def test_bundled_ffmpeg_fallback(tmp_path, monkeypatch) -> None: def test_bundled_ffmpeg_import_error(monkeypatch) -> None: + """验证 imageio-ffmpeg 不可用时最终回退为 "ffmpeg" 字符串。""" monkeypatch.delenv("FFMPEG_BIN", raising=False) monkeypatch.setattr("wov_node_ffmpeg.__main__.shutil.which", lambda _: None) monkeypatch.setitem(sys.modules, "imageio_ffmpeg", None) @@ -69,12 +79,14 @@ def test_bundled_ffmpeg_import_error(monkeypatch) -> None: def test_missing_video_uri(tmp_path) -> None: + """验证缺少 video_uri 时返回失败。""" response = invoke(_request(tmp_path, inputs={})) assert response.status == "failed" assert "video_uri" in response.error def test_missing_ffmpeg(tmp_path, monkeypatch) -> None: + """验证找不到任何 ffmpeg 时返回明确失败信息。""" monkeypatch.delenv("FFMPEG_BIN", raising=False) monkeypatch.setattr("wov_node_ffmpeg.__main__.shutil.which", lambda _: None) monkeypatch.setattr("wov_node_ffmpeg.__main__._bundled_ffmpeg", lambda: None) @@ -84,6 +96,7 @@ def test_missing_ffmpeg(tmp_path, monkeypatch) -> None: def test_ffmpeg_failure(tmp_path, monkeypatch) -> None: + """验证 ffmpeg 返回非零退出码时透传 stderr 错误。""" def fake_run(command, **kwargs): return subprocess.CompletedProcess(command, 1, stderr="boom") @@ -95,6 +108,7 @@ def test_ffmpeg_failure(tmp_path, monkeypatch) -> None: def test_entrypoint(monkeypatch) -> None: + """验证 python -m wov_node_ffmpeg 会加载 ffmpeg-extract manifest。""" module_path = Path(__file__).resolve().parent.parent / "wov_node_ffmpeg" / "__main__.py" captured = {} diff --git a/wov_node_ffmpeg/__init__.py b/wov_node_ffmpeg/__init__.py index ed9c89c..18e5c4d 100644 --- a/wov_node_ffmpeg/__init__.py +++ b/wov_node_ffmpeg/__init__.py @@ -1 +1,5 @@ -"""WOV ffmpeg media node.""" +"""WOV FFmpeg 媒体节点。 + +负责从视频/音频中提取标准化音频(默认 16kHz 单声道 WAV),为 ASR 节点提供 +统一格式的输入。 +""" diff --git a/wov_node_ffmpeg/__main__.py b/wov_node_ffmpeg/__main__.py index dcce46a..2a1a1bf 100644 --- a/wov_node_ffmpeg/__main__.py +++ b/wov_node_ffmpeg/__main__.py @@ -1,3 +1,9 @@ +"""FFmpeg 提音节点入口。 + +通过标准节点 HTTP 服务对外提供音频提取能力。ffmpeg 解析顺序为: +FFMPEG_BIN 环境变量 > PATH 中的 ffmpeg > imageio-ffmpeg 内置二进制。 +""" + from __future__ import annotations import json @@ -11,29 +17,37 @@ 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") @@ -41,14 +55,15 @@ def invoke(request: InvokeRequest) -> InvokeResponse: 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", + "-y", # 覆盖可能存在的同名输出文件。 "-i", str(video_uri), - "-vn", + "-vn", # 丢弃视频流,只保留音频。 "-ac", channels, "-ar", @@ -57,6 +72,7 @@ def invoke(request: InvokeRequest) -> InvokeResponse: ] 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", @@ -65,6 +81,7 @@ def invoke(request: InvokeRequest) -> InvokeResponse: 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))