Spaces:
Running
Running
File size: 7,721 Bytes
95f5290 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | """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
|