"""字幕 OCR 整链真实集成测试。 使用 testdata/subtitle_10s.mp4(烧录 SUB 001@1-4s、SUB 002@6-9s)与真实 glm-ocr 模型:抽帧(frame-extract)→ 逐帧 OCR(subtitle-ocr)→ 汇总 SRT, 断言烧录文字与时间轴对齐。Ollama 服务或资产缺失时自动跳过。 """ import json import urllib.request from pathlib import Path import pytest from nodes.frame_extract import invoke as frame_invoke from nodes.subtitle_ocr import invoke as ocr_invoke from wov_sdk.models import InvokeRequest OLLAMA_HOST = "http://192.168.123.70:11434" MODEL = "glm-ocr:latest" # 单体根目录:tests/ 的上一级。 WORKSPACE = Path(__file__).resolve().parent.parent VIDEO = WORKSPACE / "testdata" / "subtitle_10s.mp4" def _register_nodes() -> None: """注册全部内置节点,供 subtitle-ocr 内部调 vlm-ocr 使用。""" from wov_app import registry registry.register_all() def _ollama_reachable() -> bool: """探测 Ollama 服务与目标模型是否可用。""" try: req = urllib.request.Request( f"{OLLAMA_HOST}/api/show", data=b'{"model": "%s"}' % MODEL.encode(), headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=5) as resp: return resp.status == 200 except (urllib.error.URLError, OSError): return False @pytest.mark.integration def test_subtitle_ocr_full_chain(tmp_path) -> None: """抽帧→OCR→汇总:SRT 应含 SUB 001/SUB 002 且时间轴落在各自区间。""" if not _ollama_reachable(): pytest.skip("Ollama 服务或 glm-ocr 模型不可用,跳过真实模型集成测试") if not VIDEO.is_file(): pytest.skip("缺少 testdata/subtitle_10s.mp4 测试资产,跳过集成测试") _register_nodes() # 抽帧:1s 间隔,字幕在底部,裁切下半 30% 区域(y=0.7,h=0.3)。 frames_resp = frame_invoke( InvokeRequest( run_id="chain_fx", node_instance_id="", inputs={"video_uri": str(VIDEO)}, params={"interval_seconds": 1, "crop": [0, 0.7, 1, 0.3]}, output_dir=str(tmp_path / "frames"), ) ) assert frames_resp.status == "completed", frames_resp.error manifest = json.loads(Path(frames_resp.outputs["frames_manifest"]).read_text(encoding="utf-8")) assert len(manifest) >= 8 # OCR 汇总:真实 glm-ocr 逐帧识别。 ocr_resp = ocr_invoke( InvokeRequest( run_id="chain_ocr", node_instance_id="", inputs={"frames_manifest": str(frames_resp.outputs["frames_manifest"])}, params={"model": MODEL, "ollama_host": OLLAMA_HOST, "min_chars": 2}, output_dir=str(tmp_path / "out"), ) ) assert ocr_resp.status == "completed", ocr_resp.error srt = Path(ocr_resp.outputs["srt_uri"]).read_text(encoding="utf-8") # 两条烧录字幕都应被识别(文字可能带噪声,但至少含关键片段)。 assert "SUB" in srt # 时间轴:SUB 001 应在 1-4s,SUB 002 应在 6-9s(允许模型/抽帧容差)。 first_line = next(line for line in srt.splitlines() if "-->" in line) start = first_line.split(" --> ")[0].replace(",", ".") hours, minutes, seconds = start.split(":") total = int(hours) * 3600 + int(minutes) * 60 + float(seconds) assert total < 5