docs: 为全部代码补充中文注释并加入 AGENTS 注释规范
This commit is contained in:
@@ -16,3 +16,10 @@
|
|||||||
- Windows 和 Linux 均支持。
|
- Windows 和 Linux 均支持。
|
||||||
- 优先使用 `FFMPEG_BIN`,其次 PATH 中的 `ffmpeg`,最后使用 `imageio-ffmpeg` 内置二进制。
|
- 优先使用 `FFMPEG_BIN`,其次 PATH 中的 `ffmpeg`,最后使用 `imageio-ffmpeg` 内置二进制。
|
||||||
- 不写死 ffmpeg 路径。
|
- 不写死 ffmpeg 路径。
|
||||||
|
|
||||||
|
## 代码注释规范
|
||||||
|
|
||||||
|
- 本仓库所有源码(Python、TOML 等支持注释的文件)必须配有详细中文注释,说明模块职责、ffmpeg 解析与调用逻辑,确保后续维护人员可以快速理解代码工作原理。
|
||||||
|
- 新增或修改代码时,必须同步补充或更新对应注释;不得删除已有注释。
|
||||||
|
- 测试代码同样必须配有中文注释,说明每条测试验证的行为。
|
||||||
|
- JSON 数据文件(`node.manifest.json`)不支持注释,字段语义以 `wov-sdk` 的 `NodeManifest` 模型注释和本文档输入/输出说明为准。
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# WOV FFmpeg 节点配置:使用 uv 管理环境与依赖。
|
||||||
[project]
|
[project]
|
||||||
name = "wov-node-ffmpeg"
|
name = "wov-node-ffmpeg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -5,16 +6,20 @@ description = "WOV ffmpeg media node"
|
|||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["wov-sdk", "imageio-ffmpeg>=0.6"]
|
dependencies = ["wov-sdk", "imageio-ffmpeg>=0.6"]
|
||||||
|
|
||||||
|
# 本地路径依赖 wov-sdk;imageio-ffmpeg 用于内置 ffmpeg 回退。
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
wov-sdk = { path = "../wov-sdk" }
|
wov-sdk = { path = "../wov-sdk" }
|
||||||
|
|
||||||
|
# 开发依赖:pytest 与覆盖率工具。
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = ["pytest", "pytest-cov"]
|
dev = ["pytest", "pytest-cov"]
|
||||||
|
|
||||||
|
# pytest 配置:强制 100% 行覆盖率。
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
pythonpath = ["."]
|
pythonpath = ["."]
|
||||||
addopts = "--cov=wov_node_ffmpeg --cov-report=term-missing --cov-fail-under=100"
|
addopts = "--cov=wov_node_ffmpeg --cov-report=term-missing --cov-fail-under=100"
|
||||||
|
|
||||||
|
# 仅打包节点包本身。
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
packages = ["wov_node_ffmpeg"]
|
packages = ["wov_node_ffmpeg"]
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""FFmpeg 提音节点测试。
|
||||||
|
|
||||||
|
覆盖 ffmpeg 解析优先级、成功/失败执行、缺失输入与入口点启动等真实路径。
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import runpy
|
import runpy
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -9,6 +14,7 @@ from wov_sdk.models import InvokeRequest
|
|||||||
|
|
||||||
|
|
||||||
def _request(tmp_path, **overrides) -> InvokeRequest:
|
def _request(tmp_path, **overrides) -> InvokeRequest:
|
||||||
|
"""构造包含默认视频输入与提音参数的调用请求。"""
|
||||||
payload = {
|
payload = {
|
||||||
"run_id": "run_1",
|
"run_id": "run_1",
|
||||||
"node_instance_id": "ni_1",
|
"node_instance_id": "ni_1",
|
||||||
@@ -21,6 +27,7 @@ def _request(tmp_path, **overrides) -> InvokeRequest:
|
|||||||
|
|
||||||
|
|
||||||
def test_success(tmp_path, monkeypatch) -> None:
|
def test_success(tmp_path, monkeypatch) -> None:
|
||||||
|
"""验证成功调用会生成 audio.wav 产物。"""
|
||||||
def fake_run(command, **kwargs):
|
def fake_run(command, **kwargs):
|
||||||
output = Path(command[-1])
|
output = Path(command[-1])
|
||||||
output.parent.mkdir(parents=True, exist_ok=True)
|
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:
|
def test_configured_ffmpeg(tmp_path, monkeypatch) -> None:
|
||||||
|
"""验证 FFMPEG_BIN 环境变量优先于 PATH 查找。"""
|
||||||
fake_bin = tmp_path / "ffmpeg.exe"
|
fake_bin = tmp_path / "ffmpeg.exe"
|
||||||
fake_bin.write_bytes(b"")
|
fake_bin.write_bytes(b"")
|
||||||
monkeypatch.setenv("FFMPEG_BIN", str(fake_bin))
|
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:
|
def test_bundled_ffmpeg_fallback(tmp_path, monkeypatch) -> None:
|
||||||
|
"""验证无系统 ffmpeg 时回退到 imageio-ffmpeg 内置二进制。"""
|
||||||
monkeypatch.delenv("FFMPEG_BIN", raising=False)
|
monkeypatch.delenv("FFMPEG_BIN", raising=False)
|
||||||
monkeypatch.setattr("wov_node_ffmpeg.__main__.shutil.which", lambda _: None)
|
monkeypatch.setattr("wov_node_ffmpeg.__main__.shutil.which", lambda _: None)
|
||||||
bundled = _ffmpeg_bin()
|
bundled = _ffmpeg_bin()
|
||||||
@@ -62,6 +71,7 @@ def test_bundled_ffmpeg_fallback(tmp_path, monkeypatch) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_bundled_ffmpeg_import_error(monkeypatch) -> None:
|
def test_bundled_ffmpeg_import_error(monkeypatch) -> None:
|
||||||
|
"""验证 imageio-ffmpeg 不可用时最终回退为 "ffmpeg" 字符串。"""
|
||||||
monkeypatch.delenv("FFMPEG_BIN", raising=False)
|
monkeypatch.delenv("FFMPEG_BIN", raising=False)
|
||||||
monkeypatch.setattr("wov_node_ffmpeg.__main__.shutil.which", lambda _: None)
|
monkeypatch.setattr("wov_node_ffmpeg.__main__.shutil.which", lambda _: None)
|
||||||
monkeypatch.setitem(sys.modules, "imageio_ffmpeg", 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:
|
def test_missing_video_uri(tmp_path) -> None:
|
||||||
|
"""验证缺少 video_uri 时返回失败。"""
|
||||||
response = invoke(_request(tmp_path, inputs={}))
|
response = invoke(_request(tmp_path, inputs={}))
|
||||||
assert response.status == "failed"
|
assert response.status == "failed"
|
||||||
assert "video_uri" in response.error
|
assert "video_uri" in response.error
|
||||||
|
|
||||||
|
|
||||||
def test_missing_ffmpeg(tmp_path, monkeypatch) -> None:
|
def test_missing_ffmpeg(tmp_path, monkeypatch) -> None:
|
||||||
|
"""验证找不到任何 ffmpeg 时返回明确失败信息。"""
|
||||||
monkeypatch.delenv("FFMPEG_BIN", raising=False)
|
monkeypatch.delenv("FFMPEG_BIN", raising=False)
|
||||||
monkeypatch.setattr("wov_node_ffmpeg.__main__.shutil.which", lambda _: None)
|
monkeypatch.setattr("wov_node_ffmpeg.__main__.shutil.which", lambda _: None)
|
||||||
monkeypatch.setattr("wov_node_ffmpeg.__main__._bundled_ffmpeg", 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:
|
def test_ffmpeg_failure(tmp_path, monkeypatch) -> None:
|
||||||
|
"""验证 ffmpeg 返回非零退出码时透传 stderr 错误。"""
|
||||||
def fake_run(command, **kwargs):
|
def fake_run(command, **kwargs):
|
||||||
return subprocess.CompletedProcess(command, 1, stderr="boom")
|
return subprocess.CompletedProcess(command, 1, stderr="boom")
|
||||||
|
|
||||||
@@ -95,6 +108,7 @@ def test_ffmpeg_failure(tmp_path, monkeypatch) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_entrypoint(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"
|
module_path = Path(__file__).resolve().parent.parent / "wov_node_ffmpeg" / "__main__.py"
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
"""WOV ffmpeg media node."""
|
"""WOV FFmpeg 媒体节点。
|
||||||
|
|
||||||
|
负责从视频/音频中提取标准化音频(默认 16kHz 单声道 WAV),为 ASR 节点提供
|
||||||
|
统一格式的输入。
|
||||||
|
"""
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
"""FFmpeg 提音节点入口。
|
||||||
|
|
||||||
|
通过标准节点 HTTP 服务对外提供音频提取能力。ffmpeg 解析顺序为:
|
||||||
|
FFMPEG_BIN 环境变量 > PATH 中的 ffmpeg > imageio-ffmpeg 内置二进制。
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -11,29 +17,37 @@ from wov_sdk.server import run_node
|
|||||||
|
|
||||||
|
|
||||||
def _bundled_ffmpeg() -> str | None:
|
def _bundled_ffmpeg() -> str | None:
|
||||||
|
"""尝试获取 imageio-ffmpeg 内置的 ffmpeg 可执行文件路径。"""
|
||||||
try:
|
try:
|
||||||
import imageio_ffmpeg
|
import imageio_ffmpeg
|
||||||
|
|
||||||
return imageio_ffmpeg.get_ffmpeg_exe()
|
return imageio_ffmpeg.get_ffmpeg_exe()
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
|
# 未安装 imageio-ffmpeg 或获取失败时返回 None,交由上层回退。
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _ffmpeg_bin() -> str:
|
def _ffmpeg_bin() -> str:
|
||||||
|
"""按优先级解析 ffmpeg 可执行文件,返回最终命令路径。"""
|
||||||
|
# 显式配置优先,便于部署环境指定自定义二进制。
|
||||||
configured = os.getenv("FFMPEG_BIN")
|
configured = os.getenv("FFMPEG_BIN")
|
||||||
if configured:
|
if configured:
|
||||||
return configured
|
return configured
|
||||||
|
# 其次查找 PATH 中的系统 ffmpeg。
|
||||||
found = shutil.which("ffmpeg")
|
found = shutil.which("ffmpeg")
|
||||||
if found:
|
if found:
|
||||||
return found
|
return found
|
||||||
|
# 最后回退到 imageio-ffmpeg 内置二进制;都没有时保留 "ffmpeg" 交给调用失败处理。
|
||||||
return _bundled_ffmpeg() or "ffmpeg"
|
return _bundled_ffmpeg() or "ffmpeg"
|
||||||
|
|
||||||
|
|
||||||
def invoke(request: InvokeRequest) -> InvokeResponse:
|
def invoke(request: InvokeRequest) -> InvokeResponse:
|
||||||
|
"""提取输入视频/音频的标准化音频,产物为 audio.wav。"""
|
||||||
video_uri = request.inputs.get("video_uri")
|
video_uri = request.inputs.get("video_uri")
|
||||||
if not video_uri:
|
if not video_uri:
|
||||||
return InvokeResponse(status="failed", error="video_uri is required")
|
return InvokeResponse(status="failed", error="video_uri is required")
|
||||||
|
|
||||||
|
# 找不到可用 ffmpeg 时直接返回失败,避免子进程报晦涩错误。
|
||||||
ffmpeg = _ffmpeg_bin()
|
ffmpeg = _ffmpeg_bin()
|
||||||
if shutil.which(ffmpeg) is None and not Path(ffmpeg).is_file():
|
if shutil.which(ffmpeg) is None and not Path(ffmpeg).is_file():
|
||||||
return InvokeResponse(status="failed", error="ffmpeg not found")
|
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 = Path(request.output_dir)
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
output_path = output_dir / "audio.wav"
|
output_path = output_dir / "audio.wav"
|
||||||
|
# ASR 节点默认期望 16kHz 单声道;参数可覆盖。
|
||||||
channels = str(request.params.get("channels", 1))
|
channels = str(request.params.get("channels", 1))
|
||||||
sample_rate = str(request.params.get("sample_rate", 16000))
|
sample_rate = str(request.params.get("sample_rate", 16000))
|
||||||
command = [
|
command = [
|
||||||
ffmpeg,
|
ffmpeg,
|
||||||
"-y",
|
"-y", # 覆盖可能存在的同名输出文件。
|
||||||
"-i",
|
"-i",
|
||||||
str(video_uri),
|
str(video_uri),
|
||||||
"-vn",
|
"-vn", # 丢弃视频流,只保留音频。
|
||||||
"-ac",
|
"-ac",
|
||||||
channels,
|
channels,
|
||||||
"-ar",
|
"-ar",
|
||||||
@@ -57,6 +72,7 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
|
|||||||
]
|
]
|
||||||
result = subprocess.run(command, capture_output=True, text=True)
|
result = subprocess.run(command, capture_output=True, text=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
|
# 返回 stderr 尾部,保留最有诊断价值的错误信息。
|
||||||
return InvokeResponse(
|
return InvokeResponse(
|
||||||
status="failed",
|
status="failed",
|
||||||
error=result.stderr[-2000:] or "ffmpeg failed",
|
error=result.stderr[-2000:] or "ffmpeg failed",
|
||||||
@@ -65,6 +81,7 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
"""加载节点清单并以本模块的 invoke 处理器启动服务。"""
|
||||||
manifest_path = Path(__file__).resolve().parent.parent / "node.manifest.json"
|
manifest_path = Path(__file__).resolve().parent.parent / "node.manifest.json"
|
||||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||||
manifest = NodeManifest.from_dict(json.load(f))
|
manifest = NodeManifest.from_dict(json.load(f))
|
||||||
|
|||||||
Reference in New Issue
Block a user