Files
vrsub/tests/test_learn_translate_workflow.py
T
cat-shark a8fe133aa4 feat: whisper 新增 decode_full 无VAD整段解码参数并改为整条删除式幻觉清洗
解决转写漏句(有人说话但没识别出来)问题: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 个待决问题)。
2026-09-07 17:21:04 +08:00

108 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""学习资料转译工作流(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_helpasr 每个参数都有 _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