"""学习资料转译工作流(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