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 个待决问题)。
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""学习资料转译工作流(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
|
||||
Reference in New Issue
Block a user