为视频生成 VR 双眼字幕的单体实现:FastAPI 后端、调度器与全部节点 (提音/转写/翻译/ASS/抽帧/OCR/LLM 过滤)在单进程内运行。 - 节点协议(wov_sdk 数据模型)与分布式版保持一致,预留回退桥梁 - 工作流即数据:DAG 存于 workflows/*.json,模型/链路改动只改数据 - 调度器:拓扑顺序执行、断点续跑(产物重建)、任务暂停/继续 - 抽帧按帧间隔(select 按帧号精确取帧),VLM OCR 与 LLM 过滤使用 自适应线程池弹性并发,并打印数据处理速度进度日志 - 100% 行覆盖率(pytest --cov-fail-under=100)
175 lines
5.3 KiB
Python
Executable File
175 lines
5.3 KiB
Python
Executable File
"""wov_sdk.models 的单元测试。
|
|
|
|
测试覆盖所有数据模型的 JSON 往返序列化、字段校验和 manifest 文件加载,
|
|
确保协议模型的稳定性。
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from wov_sdk.models import (
|
|
HealthResponse,
|
|
InvokeRequest,
|
|
InvokeResponse,
|
|
NodeManifest,
|
|
ProgressEvent,
|
|
WorkflowDefinition,
|
|
WorkflowEdge,
|
|
WorkflowNode,
|
|
)
|
|
|
|
|
|
def valid_manifest() -> NodeManifest:
|
|
"""构造一个覆盖全部字段的合法 NodeManifest,供测试复用。"""
|
|
return NodeManifest(
|
|
id="echo",
|
|
name="Echo",
|
|
version="1.0.0",
|
|
capability="echo",
|
|
command=["python", "-m", "echo"],
|
|
repo_dir="wov-node-echo",
|
|
env={"PORT": "0"},
|
|
input_schema={"text": "string"},
|
|
output_schema={"text": "string"},
|
|
max_concurrency=2,
|
|
idle_ttl_seconds=15,
|
|
health_timeout_seconds=5,
|
|
keep_warm=True,
|
|
)
|
|
|
|
|
|
def test_manifest_round_trip() -> None:
|
|
"""验证 manifest 经过 to_dict/from_dict 后保持原值。"""
|
|
manifest = valid_manifest()
|
|
restored = NodeManifest.from_dict(manifest.to_dict())
|
|
assert restored == manifest
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("field", "value"),
|
|
[
|
|
("id", ""),
|
|
("name", ""),
|
|
("version", ""),
|
|
("capability", ""),
|
|
("repo_dir", ""),
|
|
("command", []),
|
|
("max_concurrency", 0),
|
|
("idle_ttl_seconds", -1),
|
|
("health_timeout_seconds", 0),
|
|
],
|
|
)
|
|
def test_manifest_validation(field: str, value: object) -> None:
|
|
"""验证必填字段为空或数值越界时抛出 ValueError。"""
|
|
manifest = valid_manifest()
|
|
setattr(manifest, field, value)
|
|
with pytest.raises(ValueError):
|
|
manifest.validate()
|
|
|
|
|
|
def test_manifest_load(tmp_path) -> None:
|
|
"""验证 NodeManifest.load 能从 JSON 文件读取并校验。"""
|
|
path = tmp_path / "node.manifest.json"
|
|
path.write_text(json.dumps(valid_manifest().to_dict()), encoding="utf-8")
|
|
loaded = NodeManifest.load(str(path))
|
|
assert loaded.id == "echo"
|
|
|
|
|
|
def test_invoke_request_round_trip() -> None:
|
|
"""验证 InvokeRequest 的 JSON 往返序列化。"""
|
|
request = InvokeRequest(
|
|
run_id="run_1",
|
|
node_instance_id="ni_1",
|
|
inputs={"text": "hello"},
|
|
params={"temperature": 0.2},
|
|
output_dir="out",
|
|
)
|
|
restored = InvokeRequest.from_dict(request.to_dict())
|
|
assert restored == request
|
|
|
|
|
|
def test_invoke_response_round_trip() -> None:
|
|
"""验证 InvokeResponse 的 JSON 往返序列化。"""
|
|
response = InvokeResponse(status="completed", outputs={"text": "hello"})
|
|
restored = InvokeResponse.from_dict(response.to_dict())
|
|
assert restored == response
|
|
|
|
|
|
def test_health_and_progress_serialization() -> None:
|
|
"""验证健康检查和进度事件模型的字典输出。"""
|
|
health = HealthResponse(status="ok", node_id="echo", version="1.0.0")
|
|
assert health.to_dict() == {
|
|
"status": "ok",
|
|
"node_id": "echo",
|
|
"version": "1.0.0",
|
|
}
|
|
|
|
progress = ProgressEvent(run_id="run_1", node_id="echo", progress=0.5, message="half")
|
|
assert progress.to_dict() == {
|
|
"run_id": "run_1",
|
|
"node_id": "echo",
|
|
"progress": 0.5,
|
|
"message": "half",
|
|
}
|
|
|
|
|
|
def test_workflow_node_and_edge_round_trip() -> None:
|
|
"""验证工作流节点与边的 JSON 往返序列化。"""
|
|
node = WorkflowNode(
|
|
id="asr",
|
|
node_type="faster-whisper",
|
|
params={"language": "ja"},
|
|
inputs={"audio_uri": "extract.audio_uri"},
|
|
)
|
|
edge = WorkflowEdge(from_node="extract", to_node="asr")
|
|
assert WorkflowNode.from_dict(node.to_dict()) == node
|
|
assert WorkflowEdge.from_dict(edge.to_dict()) == edge
|
|
assert edge.to_dict() == {"from": "extract", "to": "asr"}
|
|
|
|
assert node.to_dict()["inputs"] == {"audio_uri": "extract.audio_uri"}
|
|
|
|
|
|
def test_workflow_definition_round_trip_and_validation() -> None:
|
|
"""验证完整 DAG 定义可往返序列化并通过校验。"""
|
|
definition = WorkflowDefinition(
|
|
name="demo",
|
|
version=1,
|
|
nodes=[
|
|
WorkflowNode(id="extract", node_type="ffmpeg"),
|
|
WorkflowNode(id="asr", node_type="whisper"),
|
|
],
|
|
edges=[WorkflowEdge(from_node="extract", to_node="asr")],
|
|
entry_inputs={"video_uri": "file"},
|
|
final_outputs={"srt": "asr.srt_uri"},
|
|
)
|
|
restored = WorkflowDefinition.from_dict(definition.to_dict())
|
|
assert restored == definition
|
|
restored.validate()
|
|
|
|
|
|
def test_workflow_definition_invalid() -> None:
|
|
"""验证非法 DAG(空名、版本为 0、重复节点、未知边)被拒绝。"""
|
|
with pytest.raises(ValueError):
|
|
WorkflowDefinition(name="", version=1).validate()
|
|
|
|
with pytest.raises(ValueError):
|
|
WorkflowDefinition(name="demo", version=0).validate()
|
|
|
|
duplicate = WorkflowDefinition(
|
|
name="demo",
|
|
version=1,
|
|
nodes=[WorkflowNode(id="a", node_type="x"), WorkflowNode(id="a", node_type="y")],
|
|
)
|
|
with pytest.raises(ValueError):
|
|
duplicate.validate()
|
|
|
|
unknown_edge = WorkflowDefinition(
|
|
name="demo",
|
|
version=1,
|
|
nodes=[WorkflowNode(id="a", node_type="x")],
|
|
edges=[WorkflowEdge(from_node="a", to_node="missing")],
|
|
)
|
|
with pytest.raises(ValueError):
|
|
unknown_edge.validate()
|