解决转写漏句(有人说话但没识别出来)问题:silero VAD 对呻吟/轻语/BGM 混叠声学切段能力天然不足,把真话当非语音剔除(实测 savr-1054 全片仅 召回 115 条)。新增 decode_full 参数(默认 false 保持 VAD 现状): - decode_full=true 时强制无 VAD 整段解码 + 跳过自动 VAD 分析,救回被 剔除的弱语音(savr-1054 全片 115 条 → 340 条) - 副作用是长时寒暄套话幻觉(おやすみなさい/ご視聴ありがとうございま した 等),whisper 转录后连带时间戳整条删除(clean_japanese_ha lllucinations),不留下 '-' 占位污染下游(占位会渲染进 ASS 成减号) - llm-translate 翻译后同样整条删除中文长时寒暄幻觉(clean_srt_text) - 短时(≤15s)相同词可能是剧情真实道晚安,保留(15s 阈值实测校准) - subtitle_cleanup 由 '-' 占位式改为整条删除式 + 剩余重编号,新增 JAPANESE_HALLUCINATION_TOKENS 词表 新增工作流 learn-translate(学习资料转译+翻译字幕)示范 decode_full 用法,并确立参数标注约定:params._note_<参数名> 存放设定理由与正反例、 _node_help 放节点参数手册(_ 前缀说明键,节点执行时忽略,零运行影响)。 调研记录见 docs/调研-whisper漏句与decode_full验证.md(A/B 实验、结论 修正与 5 个待决问题)。
108 lines
4.8 KiB
Python
108 lines
4.8 KiB
Python
"""学习资料转译工作流(learn-translate)加载与标注测试。
|
||
|
||
验证新增工作流 learn-translate.json:
|
||
1. 能被 seed 正常加载入库(definition 通过 WorkflowDefinition 校验);
|
||
2. 关键参数应用了本次修复(decode_full=true、vad_filter=false)且 params 内
|
||
_note_* 说明键、_node_help 手册完整保留(不被模型/入库丢弃);
|
||
3. 标注键(_note_*/_node_help)作为 params 传给节点时不影响节点执行——
|
||
节点只读取它认识的参数键,多余的说明键被忽略(真实节点行为)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
from wov_app.db import Database
|
||
from wov_app.seed import seed_default_workflows
|
||
from wov_sdk.models import WorkflowDefinition
|
||
|
||
# 单体根目录:tests/ 的上一级。
|
||
WORKSPACE = Path(__file__).resolve().parent.parent
|
||
LEARN = WORKSPACE / "workflows" / "learn-translate.json"
|
||
|
||
|
||
def test_learn_translate_seed_loads_and_applies_fix() -> None:
|
||
"""验证 learn-translate 能被 seed 加载,asr 应用本次 decode_full 修复。"""
|
||
db = Database(WORKSPACE / "data" / "wov_test.db")
|
||
# 用临时目录避免污染真实 data
|
||
import tempfile
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
db2 = Database(Path(tmp) / "wov.db")
|
||
created = seed_default_workflows(db2)
|
||
assert created >= 4 # demo/zh-direct/ocr-subtitle/learn-translate
|
||
definition = db2.get_latest_workflow_version("learn-translate")["definition"]
|
||
asr = next(node for node in definition["nodes"] if node["id"] == "asr")
|
||
assert asr["params"]["decode_full"] is True
|
||
assert asr["params"]["vad_filter"] is False
|
||
assert asr["params"]["chunk_seconds"] == 60
|
||
assert asr["params"]["condition_on_previous_text"] is False
|
||
|
||
|
||
def test_learn_translate_notes_and_help_preserved() -> None:
|
||
"""验证 _note_* 理由与 _node_help 手册在入库后完整保留。"""
|
||
import tempfile
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
db = Database(Path(tmp) / "wov.db")
|
||
seed_default_workflows(db)
|
||
definition = db.get_latest_workflow_version("learn-translate")["definition"]
|
||
# 每个节点都应有 _node_help;asr 每个参数都有 _note_。
|
||
for node in definition["nodes"]:
|
||
assert "_node_help" in node["params"], f"{node['id']} 缺 _node_help"
|
||
assert node["params"]["_node_help"] # 非空
|
||
asr = next(node for node in definition["nodes"] if node["id"] == "asr")
|
||
for key in ("_note_decode_full", "_note_vad_filter", "_note_chunk_seconds",
|
||
"_note_condition_on_previous_text", "_note_beam_size"):
|
||
assert key in asr["params"], f"asr 缺 {key}"
|
||
# 理由需含正例/反例关键字(说明确实举例)。
|
||
assert "正例" in asr["params"]["_note_decode_full"]
|
||
assert "反例" in asr["params"]["_note_decode_full"]
|
||
|
||
|
||
def test_learn_translate_notes_do_not_break_execution(tmp_path, monkeypatch) -> None:
|
||
"""验证带 _note_*/_node_help 的 params 传给 whisper 节点不影响执行。
|
||
|
||
节点只读取它认识的键(chunk_seconds/vad_filter/decode_full 等),
|
||
额外的说明键被忽略;伪造模型确认 transcribe 收到的正是修复后参数。
|
||
"""
|
||
import sys
|
||
import types
|
||
from tests.test_nodes import FakeSegment, _install_fake_whisper, _whisper_request # 复用脚手架
|
||
|
||
captured = {}
|
||
|
||
class FullModel:
|
||
def __init__(self, *args, **kwargs):
|
||
pass
|
||
|
||
def transcribe(self, path, **kwargs):
|
||
captured["decode_full_effect"] = (
|
||
kwargs.get("vad_filter") is False and kwargs.get("vad_parameters") is None
|
||
)
|
||
return ([FakeSegment(0, 1, "ok")], None)
|
||
|
||
monkeypatch.setitem(
|
||
sys.modules, "faster_whisper", types.SimpleNamespace(WhisperModel=lambda *a, **k: FullModel())
|
||
)
|
||
from nodes.whisper import invoke as whisper_invoke
|
||
from wov_sdk.models import InvokeRequest
|
||
import wave
|
||
# 真实 WAV
|
||
wav = tmp_path / "audio.wav"
|
||
with wave.open(str(wav), "wb") as w:
|
||
w.setnchannels(1); w.setsampwidth(2); w.setframerate(16000)
|
||
w.writeframes(b"\x00\x00" * 16000)
|
||
# 带 learn-translate 工作流 asr 的完整 params(含 _note_*/_node_help)
|
||
learn = json.loads(LEARN.read_text(encoding="utf-8"))
|
||
asr_params = next(n["params"] for n in learn["definition"]["nodes"] if n["id"] == "asr")
|
||
resp = whisper_invoke(InvokeRequest(
|
||
run_id="learn_test", node_instance_id="",
|
||
inputs={"audio_uri": str(wav)},
|
||
params=asr_params,
|
||
output_dir=str(tmp_path / "out"),
|
||
))
|
||
assert resp.status == "completed"
|
||
assert captured["decode_full_effect"] is True # decode_full 生效且说明键被忽略不报错
|
||
content = Path(resp.outputs["srt_uri"]).read_text(encoding="utf-8")
|
||
assert "ok" in content
|