Compare commits
2
Commits
206275f7d6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1a8354282 | ||
|
|
812d276998 |
@@ -18,3 +18,10 @@ uv add faster-whisper
|
||||
```
|
||||
|
||||
Linux 上同样在 `wov-node-whisper` 目录执行 `uv add faster-whisper`;节点管理器会自动使用该仓库 `.venv/bin/python` 启动节点。
|
||||
|
||||
## 代码注释规范
|
||||
|
||||
- 本仓库所有源码(Python、TOML 等支持注释的文件)必须配有详细中文注释,说明模块职责、模型加载参数与 SRT 生成逻辑,确保后续维护人员可以快速理解代码工作原理。
|
||||
- 新增或修改代码时,必须同步补充或更新对应注释;不得删除已有注释。
|
||||
- 测试代码同样必须配有中文注释,说明每条测试验证的行为。
|
||||
- JSON 数据文件(`node.manifest.json`)不支持注释,字段语义以 `wov-sdk` 的 `NodeManifest` 模型注释和本文档输入/输出说明为准。
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
# WOV faster-whisper 节点配置:使用 uv 管理环境与依赖。
|
||||
[project]
|
||||
name = "wov-node-whisper"
|
||||
version = "0.1.0"
|
||||
description = "WOV faster-whisper ASR node"
|
||||
requires-python = ">=3.11"
|
||||
# 显式加入 NVIDIA 动态库包,保证 GPU 场景下 cublas/cudnn 可被加载。
|
||||
dependencies = [
|
||||
"faster-whisper>=1.2.1",
|
||||
"nvidia-cublas-cu12>=12.9.2.10",
|
||||
"nvidia-cudnn-cu12>=9.24.0.43",
|
||||
"wov-sdk",
|
||||
]
|
||||
|
||||
# 本地路径依赖 wov-sdk。
|
||||
[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_whisper --cov-report=term-missing --cov-fail-under=100"
|
||||
|
||||
# 仅打包节点包本身。
|
||||
[tool.setuptools]
|
||||
packages = ["wov_node_whisper"]
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
"""faster-whisper 转写节点测试。
|
||||
|
||||
通过注入假 faster_whisper 模块覆盖时间戳格式化、参数传递、SRT 生成、
|
||||
异常处理和入口点启动等真实代码路径。
|
||||
"""
|
||||
|
||||
import runpy
|
||||
import sys
|
||||
import types
|
||||
@@ -8,6 +14,8 @@ from wov_sdk.models import InvokeRequest
|
||||
|
||||
|
||||
class FakeSegment:
|
||||
"""模拟 faster-whisper 的分段对象,只提供转写测试需要的字段。"""
|
||||
|
||||
def __init__(self, start, end, text):
|
||||
self.start = start
|
||||
self.end = end
|
||||
@@ -15,11 +23,18 @@ class FakeSegment:
|
||||
|
||||
|
||||
class FakeWhisperModel:
|
||||
"""记录构造参数并返回固定分段的假 WhisperModel。"""
|
||||
|
||||
instances: list[tuple[tuple, dict]] = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# 记录每次构造参数,测试据此断言 device/compute_type 传递。
|
||||
FakeWhisperModel.instances.append((args, kwargs))
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def transcribe(self, path, **kwargs):
|
||||
# 返回固定两个分段:一个普通时长,一个跨小时验证时间戳格式。
|
||||
self.transcribe_args = (path, kwargs)
|
||||
return (
|
||||
[
|
||||
@@ -31,11 +46,13 @@ class FakeWhisperModel:
|
||||
|
||||
|
||||
def _install_fake_whisper(monkeypatch, model_class=FakeWhisperModel) -> None:
|
||||
"""把假 faster_whisper 模块注入 sys.modules,替代真实依赖。"""
|
||||
fake_module = types.SimpleNamespace(WhisperModel=model_class)
|
||||
monkeypatch.setitem(sys.modules, "faster_whisper", fake_module)
|
||||
|
||||
|
||||
def _request(tmp_path, **overrides) -> InvokeRequest:
|
||||
"""构造默认音频输入与日语参数的调用请求。"""
|
||||
payload = {
|
||||
"run_id": "run_1",
|
||||
"node_instance_id": "ni_1",
|
||||
@@ -48,12 +65,15 @@ def _request(tmp_path, **overrides) -> InvokeRequest:
|
||||
|
||||
|
||||
def test_format_timestamp() -> None:
|
||||
"""验证秒数到 SRT 时间戳的格式化结果。"""
|
||||
assert format_timestamp(0) == "00:00:00,000"
|
||||
assert format_timestamp(3600.5) == "01:00:00,500"
|
||||
assert format_timestamp(61.25) == "00:01:01,250"
|
||||
|
||||
|
||||
def test_success(tmp_path, monkeypatch) -> None:
|
||||
"""验证成功转写会生成 SRT 并默认使用 auto 设备/计算类型。"""
|
||||
FakeWhisperModel.instances.clear()
|
||||
_install_fake_whisper(monkeypatch)
|
||||
(tmp_path / "audio.wav").write_bytes(b"fake")
|
||||
response = invoke(_request(tmp_path))
|
||||
@@ -61,9 +81,24 @@ def test_success(tmp_path, monkeypatch) -> None:
|
||||
content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8")
|
||||
assert "第一段" in content
|
||||
assert "01:00:00,500 --> 01:00:02,250" in content
|
||||
_, kwargs = FakeWhisperModel.instances[-1]
|
||||
assert kwargs["device"] == "auto"
|
||||
assert kwargs["compute_type"] == "auto"
|
||||
|
||||
|
||||
def test_compute_type_override(tmp_path, monkeypatch) -> None:
|
||||
"""验证请求参数可以覆盖默认计算类型。"""
|
||||
FakeWhisperModel.instances.clear()
|
||||
_install_fake_whisper(monkeypatch)
|
||||
(tmp_path / "audio.wav").write_bytes(b"fake")
|
||||
response = invoke(_request(tmp_path, params={"language": "ja", "compute_type": "int8"}))
|
||||
assert response.status == "completed"
|
||||
_, kwargs = FakeWhisperModel.instances[-1]
|
||||
assert kwargs["compute_type"] == "int8"
|
||||
|
||||
|
||||
def test_model_raises(tmp_path, monkeypatch) -> None:
|
||||
"""验证模型加载失败时返回 failed 与错误信息。"""
|
||||
class BrokenModel:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise RuntimeError("model load failed")
|
||||
@@ -76,17 +111,20 @@ def test_model_raises(tmp_path, monkeypatch) -> None:
|
||||
|
||||
|
||||
def test_missing_input(tmp_path) -> None:
|
||||
"""验证缺少 audio_uri 时返回失败。"""
|
||||
response = invoke(_request(tmp_path, inputs={}))
|
||||
assert response.status == "failed"
|
||||
|
||||
|
||||
def test_missing_file(tmp_path) -> None:
|
||||
"""验证音频文件不存在时返回失败。"""
|
||||
response = invoke(_request(tmp_path))
|
||||
assert response.status == "failed"
|
||||
assert "audio file not found" in response.error
|
||||
|
||||
|
||||
def test_entrypoint(monkeypatch) -> None:
|
||||
"""验证 python -m wov_node_whisper 会加载 faster-whisper manifest。"""
|
||||
module_path = Path(__file__).resolve().parent.parent / "wov_node_whisper" / "__main__.py"
|
||||
captured = {}
|
||||
|
||||
|
||||
@@ -509,6 +509,42 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cublas-cu12"
|
||||
version = "12.9.2.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cuda-nvrtc-cu12" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/a2/c96163a0fff1839c0c9548bbdeae7b853b867009e33b9b9264adc238b1cf/nvidia_cublas_cu12-12.9.2.10-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:5572131a59c3eebeeb1c4c8144f772d49372c20124916e072a0e3fc30df421d5", size = 575012079, upload-time = "2026-04-08T18:51:47.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/c0/0a517bfe63ccd3b92eb254d264e28fca3c7cab75d07daea315250fb1bf73/nvidia_cublas_cu12-12.9.2.10-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:e4f53a8ca8c5d6e8c492d0d0a3d565ecb59a751b19cfdaa4f6da0ab2104c1702", size = 581240110, upload-time = "2026-04-08T18:52:31.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/e2/fc9a0e985249d873150276d5afb02e39a66817fedbf1a385724393e505ed/nvidia_cublas_cu12-12.9.2.10-py3-none-win_amd64.whl", hash = "sha256:623f43027d40d44ceadf0043f002bd25cf353e8f13ce90b9a87057019f560661", size = 553162896, upload-time = "2026-04-08T18:53:10.035Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cuda-nvrtc-cu12"
|
||||
version = "12.9.86"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/85/e4af82cc9202023862090bfca4ea827d533329e925c758f0cde964cb54b7/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:210cf05005a447e29214e9ce50851e83fc5f4358df8b453155d5e1918094dcb4", size = 89568129, upload-time = "2025-06-05T20:02:41.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/eb/c2295044b8f3b3b08860e2f6a912b702fc92568a167259df5dddb78f325e/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:096d4de6bda726415dfaf3198d4f5c522b8e70139c97feef5cd2ca6d4cd9cead", size = 44528905, upload-time = "2025-06-05T20:02:29.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/de/823919be3b9d0ccbf1f784035423c5f18f4267fb0123558d58b813c6ec86/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-win_amd64.whl", hash = "sha256:72972ebdcf504d69462d3bcd67e7b81edd25d0fb85a2c46d3ea3517666636349", size = 76408187, upload-time = "2025-06-05T20:12:27.819Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cudnn-cu12"
|
||||
version = "9.24.0.43"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas-cu12" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/f1/cd42563325fa827f54ff30da05686c747652bdbd4cb5654cea54d7d0ad4f/nvidia_cudnn_cu12-9.24.0.43-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:a42996943f0cd78ddfd61c8bf59361672a19b63e0491aa22a53d6fe63a3f854a", size = 856490582, upload-time = "2026-07-02T16:21:30.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/13/b8887c869cf2471339a24b60d3c28e761facbb534935f572b61423371abb/nvidia_cudnn_cu12-9.24.0.43-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:f424192dd85e7d29f44be18df2dae4c80d32c67a29c0d42f5c283c40cfdf871c", size = 799083985, upload-time = "2026-07-02T16:25:37.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/28/2c9a2a97a8b3fedcf74a14f38fd5edfae12274380a829fdc6b16ce29be4c/nvidia_cudnn_cu12-9.24.0.43-py3-none-win_amd64.whl", hash = "sha256:cbd41a0ab084422c936dc9fb2fc89be5ea9a85bc421c6f23d0243bdfc945fbef", size = 737103728, upload-time = "2026-07-02T16:30:10.901Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime"
|
||||
version = "1.28.0"
|
||||
@@ -791,6 +827,8 @@ version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "faster-whisper" },
|
||||
{ name = "nvidia-cublas-cu12" },
|
||||
{ name = "nvidia-cudnn-cu12" },
|
||||
{ name = "wov-sdk" },
|
||||
]
|
||||
|
||||
@@ -803,6 +841,8 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "faster-whisper", specifier = ">=1.2.1" },
|
||||
{ name = "nvidia-cublas-cu12", specifier = ">=12.9.2.10" },
|
||||
{ name = "nvidia-cudnn-cu12", specifier = ">=9.24.0.43" },
|
||||
{ name = "wov-sdk", directory = "../wov-sdk" },
|
||||
]
|
||||
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
"""WOV faster-whisper ASR node."""
|
||||
"""WOV faster-whisper 语音转写节点。
|
||||
|
||||
把 FFmpeg 节点产出的标准化音频转写为 SRT 字幕,供后续 LLM 翻译节点使用。
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
"""faster-whisper ASR 节点入口。
|
||||
|
||||
使用 faster-whisper 加载 Whisper 模型,将音频转写为带时间轴的 SRT 文件。
|
||||
模型、设备与计算类型均可通过参数或环境变量配置。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@@ -9,6 +15,8 @@ from wov_sdk.server import run_node
|
||||
|
||||
|
||||
def format_timestamp(seconds: float) -> str:
|
||||
"""把秒数格式化为 SRT 时间戳,例如 01:00:00,500。"""
|
||||
# 先换算成毫秒再逐级拆分为时/分/秒/毫秒,避免浮点误差。
|
||||
total_ms = int(seconds * 1000)
|
||||
hours, remainder = divmod(total_ms, 3600000)
|
||||
minutes, remainder = divmod(remainder, 60000)
|
||||
@@ -17,28 +25,34 @@ def format_timestamp(seconds: float) -> str:
|
||||
|
||||
|
||||
def invoke(request: InvokeRequest) -> InvokeResponse:
|
||||
"""转写音频并生成 SRT 字幕,产物为 transcript.srt。"""
|
||||
audio_uri = request.inputs.get("audio_uri")
|
||||
if not audio_uri:
|
||||
return InvokeResponse(status="failed", error="audio_uri is required")
|
||||
|
||||
# 文件不存在时提前失败,避免进入耗时的模型加载流程。
|
||||
audio_path = Path(audio_uri)
|
||||
if not audio_path.is_file():
|
||||
return InvokeResponse(status="failed", error="audio file not found")
|
||||
|
||||
try:
|
||||
# 延迟导入 faster-whisper,保证健康检查等轻量路径不依赖重型依赖。
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
# 参数优先于环境变量;模型路径缺省使用 faster-whisper 的 large-v3。
|
||||
model_path = str(
|
||||
request.params.get("model_path")
|
||||
or os.getenv("WHISPER_MODEL_PATH", "large-v3")
|
||||
)
|
||||
device = str(request.params.get("device") or os.getenv("WHISPER_DEVICE", "auto"))
|
||||
compute_type = str(request.params.get("compute_type") or "float16")
|
||||
# auto 让 faster-whisper 根据硬件自动选择 float16/int8 等计算类型。
|
||||
compute_type = str(request.params.get("compute_type") or "auto")
|
||||
model = WhisperModel(
|
||||
model_path,
|
||||
device=device,
|
||||
compute_type=compute_type,
|
||||
)
|
||||
# language 默认日语,vad_filter 过滤静音段以提升转写质量。
|
||||
segments, _info = model.transcribe(
|
||||
str(audio_path),
|
||||
language=str(request.params.get("language", "ja")),
|
||||
@@ -49,6 +63,7 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
|
||||
output_dir = Path(request.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = output_dir / "transcript.srt"
|
||||
# 按 SRT 标准输出:序号、时间轴、文本和空行交替。
|
||||
lines: list[str] = []
|
||||
for index, segment in enumerate(segments, start=1):
|
||||
lines.extend(
|
||||
@@ -62,10 +77,12 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
|
||||
output_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
return InvokeResponse(status="completed", outputs={"srt_uri": str(output_path)})
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# 模型加载或转写异常统一转换为 failed 响应,不让节点进程退出。
|
||||
return InvokeResponse(status="failed", error=str(exc))
|
||||
|
||||
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user