为视频生成 VR 双眼字幕的单体实现:FastAPI 后端、调度器与全部节点 (提音/转写/翻译/ASS/抽帧/OCR/LLM 过滤)在单进程内运行。 - 节点协议(wov_sdk 数据模型)与分布式版保持一致,预留回退桥梁 - 工作流即数据:DAG 存于 workflows/*.json,模型/链路改动只改数据 - 调度器:拓扑顺序执行、断点续跑(产物重建)、任务暂停/继续 - 抽帧按帧间隔(select 按帧号精确取帧),VLM OCR 与 LLM 过滤使用 自适应线程池弹性并发,并打印数据处理速度进度日志 - 100% 行覆盖率(pytest --cov-fail-under=100)
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
"""字幕 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
|