feat: VRSub 单体应用(WOV 单机版)初始提交
为视频生成 VR 双眼字幕的单体实现:FastAPI 后端、调度器与全部节点 (提音/转写/翻译/ASS/抽帧/OCR/LLM 过滤)在单进程内运行。 - 节点协议(wov_sdk 数据模型)与分布式版保持一致,预留回退桥梁 - 工作流即数据:DAG 存于 workflows/*.json,模型/链路改动只改数据 - 调度器:拓扑顺序执行、断点续跑(产物重建)、任务暂停/继续 - 抽帧按帧间隔(select 按帧号精确取帧),VLM OCR 与 LLM 过滤使用 自适应线程池弹性并发,并打印数据处理速度进度日志 - 100% 行覆盖率(pytest --cov-fail-under=100)
This commit is contained in:
Executable
+140
@@ -0,0 +1,140 @@
|
||||
"""工作流管理路由。
|
||||
|
||||
提供工作流的创建、查询、校验、发布和删除能力。工作流以版本化 DAG 数据保存,
|
||||
不写死在业务代码中。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wov_app.db import Database
|
||||
from wov_app.schemas import WorkflowCreate
|
||||
from wov_sdk.models import WorkflowDefinition
|
||||
|
||||
router = APIRouter(prefix="/api/admin/workflows", tags=["workflows"])
|
||||
|
||||
|
||||
def _get_db() -> Database:
|
||||
"""从应用状态延迟获取数据库实例。"""
|
||||
from wov_app.main import app
|
||||
|
||||
return app.state.db
|
||||
|
||||
|
||||
def _slugify(value: str) -> str:
|
||||
"""把工作流名称转换为小写连字符 ID;无有效字符时生成随机 ID。"""
|
||||
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
||||
return slug or uuid.uuid4().hex[:8]
|
||||
|
||||
|
||||
def _validate_definition(raw: dict) -> WorkflowDefinition:
|
||||
"""解析并校验 DAG 定义,非法时转换为 422 HTTP 异常。"""
|
||||
try:
|
||||
definition = WorkflowDefinition.from_dict(raw)
|
||||
definition.validate()
|
||||
return definition
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_workflows(db: Database = Depends(_get_db)) -> list[dict]:
|
||||
"""返回全部工作流概要。"""
|
||||
return db.list_workflows()
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_workflow(
|
||||
payload: WorkflowCreate,
|
||||
db: Database = Depends(_get_db),
|
||||
) -> dict:
|
||||
"""创建新工作流或为已有工作流追加一个版本。"""
|
||||
definition = _validate_definition(payload.definition)
|
||||
# 未显式指定 ID 时由名称生成;已有工作流则版本号递增。
|
||||
workflow_id = payload.id or _slugify(payload.name)
|
||||
existing = db.get_workflow(workflow_id)
|
||||
version = (existing or {}).get("latest_version", 0) + 1
|
||||
# 每次创建都保存新版本,发布操作只切换 published 标记。
|
||||
db.upsert_workflow(
|
||||
{
|
||||
"id": workflow_id,
|
||||
"name": payload.name,
|
||||
"description": payload.description,
|
||||
"published": 0,
|
||||
"latest_version": version,
|
||||
}
|
||||
)
|
||||
db.create_workflow_version(workflow_id, version, definition.to_dict())
|
||||
return {
|
||||
"id": workflow_id,
|
||||
"name": payload.name,
|
||||
"description": payload.description,
|
||||
"published": False,
|
||||
"latest_version": version,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{workflow_id}")
|
||||
def get_workflow(workflow_id: str, db: Database = Depends(_get_db)) -> dict:
|
||||
"""返回工作流概要及最新版本定义。"""
|
||||
workflow = db.get_workflow(workflow_id)
|
||||
if workflow is None:
|
||||
raise HTTPException(status_code=404, detail="workflow not found")
|
||||
latest = db.get_latest_workflow_version(workflow_id)
|
||||
workflow["latest_version_data"] = latest
|
||||
return workflow
|
||||
|
||||
|
||||
@router.delete("/{workflow_id}")
|
||||
def delete_workflow(workflow_id: str, db: Database = Depends(_get_db)) -> dict:
|
||||
"""删除工作流及其版本、任务和产物记录。"""
|
||||
if db.get_workflow(workflow_id) is None:
|
||||
raise HTTPException(status_code=404, detail="workflow not found")
|
||||
db.delete_workflow(workflow_id)
|
||||
return {"deleted": workflow_id}
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/validate")
|
||||
def validate_workflow(
|
||||
workflow_id: str,
|
||||
definition: dict,
|
||||
db: Database = Depends(_get_db),
|
||||
) -> dict:
|
||||
"""在不保存的情况下校验一份 DAG 定义。"""
|
||||
if db.get_workflow(workflow_id) is None:
|
||||
raise HTTPException(status_code=404, detail="workflow not found")
|
||||
parsed = _validate_definition(definition)
|
||||
return {"valid": True, "node_ids": [node.id for node in parsed.nodes]}
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/publish")
|
||||
def publish_workflow(workflow_id: str, db: Database = Depends(_get_db)) -> dict:
|
||||
"""把工作流标记为已发布,使其出现在用户应用中心。"""
|
||||
workflow = db.get_workflow(workflow_id)
|
||||
if workflow is None:
|
||||
raise HTTPException(status_code=404, detail="workflow not found")
|
||||
if workflow["latest_version"] == 0:
|
||||
raise HTTPException(status_code=422, detail="workflow has no version")
|
||||
# 发布只是状态切换,不修改已保存的版本数据。
|
||||
db.upsert_workflow(
|
||||
{
|
||||
"id": workflow_id,
|
||||
"name": workflow["name"],
|
||||
"description": workflow["description"],
|
||||
"published": 1,
|
||||
"latest_version": workflow["latest_version"],
|
||||
}
|
||||
)
|
||||
return {"published": workflow_id}
|
||||
|
||||
|
||||
@router.get("/{workflow_id}/versions")
|
||||
def list_versions(workflow_id: str, db: Database = Depends(_get_db)) -> list[dict]:
|
||||
"""返回工作流全部版本定义。"""
|
||||
if db.get_workflow(workflow_id) is None:
|
||||
raise HTTPException(status_code=404, detail="workflow not found")
|
||||
return db.list_workflow_versions(workflow_id)
|
||||
Reference in New Issue
Block a user