Spaces:
Running
Running
stouchinglhh
Claude
sync processor from GitLab c697b6e: 版本 1.19.4 / 前端 1.12.4 —— 服务端 i18n(首屏不再闪英文)、推理模块并入模型页、前端抽出 shared/、缓存键改用前端号
bb59211 Download processor/app/processing/cleaning/checks/__init__.py from Stouching-Robotics/DataFlowing: direct link, hf CLI and curl.
- Browser
- Download file 13.1 kB
-
https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/processor/app/processing/cleaning/checks/__init__.py
- Command line
-
hf download hf://spaces/Stouching-Robotics/DataFlowing/processor/app/processing/cleaning/checks/__init__.py
-
curl -L -o __init__.py https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/processor/app/processing/cleaning/checks/__init__.py
13.1 kB
| """清洗检查项注册表 —— 一个文件 = 一个检查项,自动发现。 | |
| 与 app/processing/modules/ 同一套思路(见那里的 __init__.py): | |
| for _m in pkgutil.iter_modules(__path__): | |
| importlib.import_module(...) | |
| 新增检查项只需在 checks/ 下放一个 .py,内部用 @register_check 装饰类定义。 | |
| 检查项**按信道/模态声明需求**,不按"这个 episode 是 UMI 还是 EGO"。 | |
| 所以同一份检查项集合能自动适配任何数据组合: | |
| UMI 夹爪来了 → 只跑需要 slam/force/gripper_state 的那些 | |
| 触觉手套来了 → 只跑需要 tactile 的那些 | |
| EGO 来了 → 只跑需要 rgb/keypoints 的那些 | |
| 这就是 PDF §2「用采集模板决定跑哪些清洗规则」的落地方式。 | |
| """ | |
| from __future__ import annotations | |
| import importlib | |
| import pkgutil | |
| from dataclasses import dataclass, field as dc_field | |
| from pathlib import Path | |
| from typing import Any, Callable, Iterable | |
| from app.processing.cleaning.contract import ( | |
| Finding, Range, PASS, WARN, FAIL, PENDING, ERROR, | |
| SEVERITY_LABELS, worst, is_blocking, | |
| ) | |
| __all__ = [ | |
| "CheckContext", "StreamEvidence", "CleaningCheck", "register_check", | |
| "get_check", "all_checks", "all_check_specs", "applicable_checks", | |
| "checks_for_device_cards", | |
| "Finding", "Range", "PASS", "WARN", "FAIL", "PENDING", "ERROR", | |
| "SEVERITY_LABELS", "worst", "is_blocking", | |
| ] | |
| _registry: dict[str, "CleaningCheck"] = {} | |
| class StreamEvidence: | |
| """一条数据流的证据 —— 由 evidence.py 采集,检查项**只读不采**。 | |
| 把"读磁盘"和"做判断"分开,是让检查项可纯测的关键:测试直接构造本对象, | |
| 不需要真的 mp4 / parquet / cv2。 | |
| """ | |
| key: str # 流标识,如 "umi_rgb" / "slam_trajectory" | |
| channel: str = "" # rgb | depth | slam | tactile | ... | |
| path: str = "" # 相对批次目录的路径(仅用于报错定位) | |
| # video_quality 的报告(黑屏/冻结/丢帧/解码)。非视频流为空 dict。 | |
| # 结构见 app/video_quality.py::_check_stream 的返回。 | |
| video: dict[str, Any] = dc_field(default_factory=dict) | |
| # parquet 概览 | |
| columns: tuple[str, ...] = () | |
| rows: int = 0 | |
| fps: float = 0.0 | |
| # 逐帧数值序列(按帧对齐)。evidence.py 采集时**派生好的**,检查项不自己算。 | |
| # 例:SLAM 流 → {"pose": ((x,y,z,qx,qy,qz,qw), ...), "t": (...)} | |
| # 夹爪流 → {"gripper_state": ((open, gripped, raw), ...), | |
| # "force": ((fx,fy,fz), ...)} | |
| # ★ 派生复用 app/umi_slam_action.py 的函数,保证与 action 口径同源。 | |
| series: dict[str, tuple] = dc_field(default_factory=dict) | |
| class CheckContext: | |
| """检查项执行上下文。 | |
| IO 走注入(``read_columns`` / ``probe_video`` …),这样检查项本体是纯逻辑, | |
| 测试可以喂假数据而不需要真的 mp4 或 cv2。 | |
| """ | |
| batch_dir: Path # 批次目录(含 data/ videos/ meta/) | |
| episode_id: str = "" | |
| channels: frozenset[str] = frozenset() # 实际存在的信道 | |
| modalities: tuple[str, ...] = () # 实际存在的设备模态 | |
| params: dict[str, Any] = dc_field(default_factory=dict) # 节点配置里的阈值 | |
| duration_sec: float = 0.0 | |
| fps: float = 0.0 | |
| progress: Callable[[float], None] = lambda _value: None | |
| # ── 证据包(evidence.py 采集好,检查项只读)──────────────── | |
| streams: tuple["StreamEvidence", ...] = () | |
| # parquet 侧的汇总:行数 / 时间戳列等,跨流共用的信息 | |
| data_rows: int = 0 | |
| data_columns: tuple[str, ...] = () | |
| # ── 注入式 IO ──────────────────────────────────────────── | |
| read_columns: Callable[[Path], list[str]] = lambda _p: [] | |
| read_head: Callable[[Path, int], list[dict]] = lambda _p, _n: [] | |
| probe_video: Callable[[Path], dict] = lambda _p: {} | |
| # ── 证据查询便捷方法 ────────────────────────────────────── | |
| def streams_of(self, channel: str) -> tuple["StreamEvidence", ...]: | |
| """取某信道的全部流。检查项用它筛选自己要看的流。""" | |
| return tuple(item for item in self.streams if item.channel == channel) | |
| def param(self, name: str, default: Any = None) -> Any: | |
| """取阈值配置;节点未配置时回落到检查项声明的默认值。""" | |
| value = self.params.get(name) | |
| return default if value is None else value | |
| class CleaningCheck: | |
| """清洗检查项基类。 | |
| 子类填元数据 + 实现 ``run()``,用 ``@register_check`` 装饰即可被自动发现。 | |
| """ | |
| slug: str = "" # "video.frame_drop" | |
| label: str = "" | |
| version: str = "1.0" # 改了要 bump —— 进 ruleset_revision | |
| description: str = "" | |
| # 必需的信道/设备模态;全部满足才跑 | |
| requires_channels: tuple[str, ...] = () | |
| requires_modality: tuple[str, ...] = () | |
| # 需要 ≥2 类设备模态同时存在(PDF 的"跨模态联合检查") | |
| cross_modal: bool = False | |
| # ★ 本项属于哪些【设备卡片】—— 前端按它把检查项分到设置面板的各 tab。 | |
| # | |
| # 取值来自 contract.DEVICE_MODALITIES,空元组 = 不绑定单个设备 | |
| # (跨设备检查用,它们只在"跨设备" tab 里出现)。 | |
| # | |
| # 为什么用声明而不是靠目录分:同一个检查项往往多张卡片都要用 —— | |
| # ``video.frame_drop`` 对 RGB Camera、RGB-D Camera、UMI Gripper 都成立, | |
| # 按目录分只能放一处。声明式的话它能同时出现在多个 tab 里, | |
| # 且各自的阈值配置互不影响(见 node.data.config.devices.<卡片>.params)。 | |
| device_cards: tuple[str, ...] = () | |
| default_severity: str = FAIL | |
| # 阈值默认值 —— 节点配置可覆盖。PDF 明确要求"阈值不要照搬", | |
| # 所以这里给的是**平台初始值**,必须在真实数据上验证后再固化。 | |
| default_params: dict[str, Any] = dc_field(default_factory=dict) | |
| def param(self, ctx: "CheckContext", name: str, fallback: Any = None) -> Any: | |
| """取阈值。顺序:节点配置 → 本项声明的 default_params → fallback。 | |
| 检查项一律用这个方法读阈值,不要写 ``ctx.params.get(..., 字面量)`` —— | |
| 那样声明的 default_params 就成了死代码,改它不生效。 | |
| """ | |
| value = (ctx.params or {}).get(name) | |
| if value is not None: | |
| return value | |
| if name in self.default_params: | |
| return self.default_params[name] | |
| return fallback | |
| def run(self, ctx: CheckContext) -> list[Finding]: | |
| raise NotImplementedError | |
| # ── 便捷构造 ───────────────────────────────────────────── | |
| def finding(self, ctx: CheckContext, status: str, *, | |
| stream: str = "", message: str = "", | |
| metrics: dict | None = None, | |
| ranges: Iterable[Range] = ()) -> Finding: | |
| return Finding( | |
| rule=self.slug, version=self.version, status=status, | |
| scope="cross_modal" if self.cross_modal else "stream", | |
| stream=stream, message=message, metrics=dict(metrics or {}), | |
| ranges=tuple(ranges), | |
| ) | |
| def checks_for_device_cards(cards: Iterable[str]) -> dict[str, list[CleaningCheck]]: | |
| """按【设备卡片】把检查项分组 —— 前端设置面板的 tab 就按这个渲染。 | |
| 只返回传进来的卡片(= 工作流里实际连接的设备),未连接的不会出现。 | |
| 一个检查项可能落在多个卡片下(如 ``video.frame_drop`` 对每种带摄像头的 | |
| 设备都成立),各 tab 里的阈值配置互不影响。 | |
| 例:: | |
| checks_for_device_cards(["rgbd_camera", "glove_sensor"]) | |
| → {"rgbd_camera": [video.frame_drop, video.black_screen, ...], | |
| "glove_sensor": [glove.tactile_health, ...]} | |
| """ | |
| wanted = [str(card) for card in cards] | |
| grouped: dict[str, list[CleaningCheck]] = {card: [] for card in wanted} | |
| for check in all_checks(): | |
| for card in check.device_cards: | |
| if card in grouped: | |
| grouped[card].append(check) | |
| return grouped | |
| def register_check(cls): | |
| """按 slug 注册检查项(装饰器)。""" | |
| if not cls.slug: | |
| raise ValueError(f"Check {cls.__name__} has no slug") | |
| if cls.slug in _registry: | |
| raise ValueError(f"Duplicate check slug: {cls.slug}") | |
| _registry[cls.slug] = cls() | |
| return cls | |
| def get_check(slug: str) -> CleaningCheck | None: | |
| return _registry.get(slug) | |
| def all_checks() -> list[CleaningCheck]: | |
| """全部已注册检查项(按 slug 排序,保证 ruleset_revision 稳定)。""" | |
| return [_registry[key] for key in sorted(_registry)] | |
| def all_check_specs() -> list[dict]: | |
| """检查项目录 —— 给 API / UI 用。 | |
| 前端配置面板直接渲染这个:按 ``category`` 分组,每个检查项一行开关, | |
| 下面按 ``default_params`` 动态生成输入框。新增检查项前端零改动。 | |
| """ | |
| return [ | |
| { | |
| "slug": check.slug, | |
| # 分类取自 slug 前缀(video.frame_drop → video),与 checks/ 下的 | |
| # 目录名一致。前端按它分组。 | |
| "category": check.slug.split(".", 1)[0] if "." in check.slug else "other", | |
| "label": check.label, | |
| "version": check.version, | |
| "description": check.description, | |
| "requires_channels": list(check.requires_channels), | |
| "requires_modality": list(check.requires_modality), | |
| "cross_modal": check.cross_modal, | |
| "device_cards": list(check.device_cards), | |
| "default_severity": check.default_severity, | |
| "default_params": dict(check.default_params), | |
| } | |
| for check in all_checks() | |
| ] | |
| def applicable_checks(channels: Iterable[str], | |
| modalities: Iterable[str], | |
| *, | |
| cross_modal_active: bool = False, | |
| enabled: Iterable[str] | None = None, | |
| disabled: Iterable[str] | None = None) -> list[CleaningCheck]: | |
| """筛出该跑哪些检查项。纯函数。 | |
| - ``requires_channels`` / ``requires_modality`` 必须**全部**具备 | |
| - ``cross_modal`` 的额外要求至少两类模态同时存在 | |
| - ``enabled`` / ``disabled`` 是节点配置里的按类型开关 | |
| """ | |
| have_channels = {str(item) for item in channels} | |
| have_modalities = {str(item) for item in modalities} | |
| allow = {str(item) for item in enabled} if enabled is not None else None | |
| deny = {str(item) for item in disabled or ()} | |
| result: list[CleaningCheck] = [] | |
| for check in all_checks(): | |
| if not set(check.requires_channels) <= have_channels: | |
| continue | |
| if not set(check.requires_modality) <= have_modalities: | |
| continue | |
| # ★ 设备卡片闸门 —— 声明了归属的检查项只在【对应设备】上跑。 | |
| # | |
| # 光按信道筛不够:`umi.action_present` 只要"有 action 列"就满足, | |
| # 而手套项目同样有一列(采集端写的全零占位)—— 结果 UMI 的检查跑到了 | |
| # 手套项目上、报出一个对方根本不关心的失败。 | |
| # 空 device_cards = 跨设备检查,不设闸门。 | |
| if check.device_cards and not (set(check.device_cards) & have_modalities): | |
| continue | |
| if check.cross_modal and not cross_modal_active: | |
| continue | |
| if allow is not None and check.slug not in allow: | |
| continue | |
| if check.slug in deny: | |
| continue | |
| result.append(check) | |
| return result | |
| # 自动发现检查项。 | |
| # | |
| # 用 walk_packages 而非 iter_modules —— 前者递归进子包,后者只认平铺的 .py。 | |
| # 检查项按数据类型分类放在子包里,目录即分类,便于定位: | |
| # | |
| # checks/video/frame_drop.py 视频 | |
| # checks/umi/slam_continuity.py UMI 夹爪 | |
| # checks/glove/baseline_drift.py 触觉手套 | |
| # checks/ego/hand_side.py EGO 头戴 | |
| # checks/depth/presence.py 深度 | |
| # checks/cross/video_data_sync.py 跨模态 | |
| # | |
| # 子包不需要写任何注册代码 —— 空 __init__.py 即可,里面的模块会被自动导入。 | |
| # 新增一项检查 = 在对应目录放一个 .py,注册表/节点/前端都不用改。 | |
| for _info in pkgutil.walk_packages(__path__, prefix=f"{__name__}."): | |
| if _info.name.rsplit(".", 1)[-1].startswith("_"): | |
| continue | |
| importlib.import_module(_info.name) | |