quyu666's picture
sync processor from GitLab 32d515b: glove display fixes, GPU acceleration, AI annotation propose-clip fix
95f5290
Raw History Blame Contribute Delete
7.72 kB
"""Processing Module Framework — pluggable pipeline nodes.
每个功能(相机输入、骨骼识别、手套、标注、审核、导出)是一个独立模块:
一个文件 = 一个模块(元数据 + run()),放在 app/processing/modules/ 下,
自动发现注册。前端画布与 worker 执行都从注册表取模块定义。
新增自定义模块三步:
1. 复制 modules/example 模板为 app/processing/modules/<slug>.py
2. 填写 slug/label/category/inputs/outputs/config_schema 等元数据,实现 run(ctx)
3. 重启后端与 worker —— 前端面板自动出现可拖拽卡片,worker 自动执行
"""
from __future__ import annotations
from dataclasses import dataclass, field as dc_field
from pathlib import Path
from typing import Any, Callable
def field(name: str, kind: str, label: str, default=None, **extra) -> dict:
"""config_schema 单字段定义: {"name","type","label","default",**extra}。"""
value = {"name": name, "type": kind, "label": label, "default": default}
value.update(extra)
return value
@dataclass
class ModuleResult:
"""Returned by a ProcessingModule after execution (legacy, kept for compat)."""
success: bool
data: dict[str, Any] = dc_field(default_factory=dict)
outputs: dict[str, "ArtifactRef"] = dc_field(default_factory=dict)
metrics: dict[str, Any] = dc_field(default_factory=dict)
error: str | None = None
@dataclass
class ArtifactRef:
"""Portable reference to a workflow input/output artifact."""
kind: str
path: str | None = None
source_key: str | None = None
schema_version: str = "1.0"
metadata: dict[str, Any] = dc_field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"kind": self.kind,
"path": self.path,
"source_key": self.source_key,
"schema_version": self.schema_version,
"metadata": self.metadata,
}
@classmethod
def from_dict(cls, value: dict[str, Any]) -> "ArtifactRef":
return cls(
kind=str(value.get("kind", "unknown")),
path=value.get("path"),
source_key=value.get("source_key"),
schema_version=str(value.get("schema_version", "1.0")),
metadata=dict(value.get("metadata") or {}),
)
@dataclass(frozen=True)
class ModuleDescriptor:
"""Metadata shared by the backend module registry and card palette."""
slug: str
version: str
category: str
label: str
icon: str
color: str
inputs: tuple[dict[str, str], ...] = ()
outputs: tuple[dict[str, str], ...] = ()
default_config: dict[str, Any] = dc_field(default_factory=dict)
config_schema: tuple[dict[str, Any], ...] = ()
execution_target: str = "worker"
capabilities: tuple[str, ...] = ()
def to_dict(self) -> dict[str, Any]:
return {
"type": self.slug,
"slug": self.slug,
"version": self.version,
"category": self.category,
"label": self.label,
"icon": self.icon,
"color": self.color,
"inputs": list(self.inputs),
"outputs": list(self.outputs),
"defaultConfig": self.default_config,
"default_config": self.default_config,
"configSchema": list(self.config_schema),
"config_schema": list(self.config_schema),
"executionTarget": self.execution_target,
"execution_target": self.execution_target,
"capabilities": list(self.capabilities),
}
class ModuleSkip(Exception):
"""模块主动跳过(无有效输入等)—— 节点标记 skipped,不视为失败。"""
def __init__(self, reason: str):
self.reason = reason
super().__init__(reason)
@dataclass
class JobContext:
"""Worker 与模块之间的统一执行上下文。"""
node_id: str
node_type: str
config: dict[str, Any]
job: dict[str, Any]
input_root: Path # 批次解压目录(源文件扫描)
output_root: Path # 输出根(outputs/)
incoming: dict[str, ArtifactRef] # targetHandle → 上游 ArtifactRef(源节点为空 dict)
progress: Callable[[float], None] # 本节点进度 0..1
node_states: dict # 节点状态累加器
@property
def output_dir(self) -> Path:
d = self.output_root / self.node_id
d.mkdir(parents=True, exist_ok=True)
return d
def ref(self, kind: str, path: Path, source_key: str | None = None,
metadata: dict | None = None) -> ArtifactRef:
"""构造 ArtifactRef,路径自动相对化:input_root 下 → 相对 input_root,否则相对 output_root。"""
try:
rel = path.resolve().relative_to(self.input_root.resolve())
except ValueError:
rel = path.resolve().relative_to(self.output_root.resolve())
return ArtifactRef(kind=kind, path=str(rel).replace("\\", "/"),
source_key=source_key, metadata=metadata or {})
def resolve(self, ref: ArtifactRef) -> Path | None:
"""按 input_root/output_root 依次解析 ArtifactRef 路径。"""
if not ref or not ref.path:
return None
p = self.input_root / ref.path
if p.exists():
return p
p = self.output_root / ref.path
return p if p.exists() else None
def find_videos(self) -> list[Path]:
"""按 config 的 source_keys/source_key/position 匹配批次内视频(见 batch.py)。"""
from app.processing.batch import find_videos
return find_videos(self.input_root, self.config)
def find_parquet(self) -> list[Path]:
"""批次内全部 parquet(排除 auto_labels/hand_kpts 等合并产物)。"""
from app.processing.batch import find_parquet
return find_parquet(self.input_root)
def skip(self, reason: str):
raise ModuleSkip(reason)
class ProcessingModule:
"""Base class for a pipeline processing node.
Subclass, fill in the metadata class attributes, implement ``run()``,
and decorate with ``@register`` from app.processing.registry — the
module appears in the Workflow Studio palette and runs in the worker
automatically.
"""
slug: str = ""
version: str = "1.0"
category: str = "process" # input | process | review | export
label: str = ""
icon: str = "ant-design:appstore-outlined"
color: str = "#64748b"
inputs: tuple[dict[str, str], ...] = ()
outputs: tuple[dict[str, str], ...] = ()
default_config: dict[str, Any] = dc_field(default_factory=dict)
config_schema: tuple[dict[str, Any], ...] = ()
execution_target: str = "worker"
capabilities: tuple[str, ...] = ()
@classmethod
def descriptor(cls) -> ModuleDescriptor:
return ModuleDescriptor(
slug=cls.slug, version=cls.version, category=cls.category, label=cls.label,
icon=cls.icon, color=cls.color, inputs=tuple(cls.inputs),
outputs=tuple(cls.outputs), default_config=dict(cls.default_config),
config_schema=tuple(cls.config_schema), execution_target=cls.execution_target,
capabilities=tuple(cls.capabilities),
)
def run(self, ctx: JobContext) -> dict[str, ArtifactRef] | None:
"""执行入口。返回输出 handle → ArtifactRef;None 或 raise ModuleSkip → 节点 skipped。"""
raise NotImplementedError