Spaces:
Running
Running
stouchinglhh
Claude
sync processor from GitLab c697b6e: 版本 1.19.4 / 前端 1.12.4 —— 服务端 i18n(首屏不再闪英文)、推理模块并入模型页、前端抽出 shared/、缓存键改用前端号
bb59211 Download processor/app/processing/cleaning/engine.py from Stouching-Robotics/DataFlowing: direct link, hf CLI and curl.
- Browser
- Download file 13 kB
-
https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/processor/app/processing/cleaning/engine.py
- Command line
-
hf download hf://spaces/Stouching-Robotics/DataFlowing/processor/app/processing/cleaning/engine.py
-
curl -L -o engine.py https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/processor/app/processing/cleaning/engine.py
13 kB
| """质检引擎 —— 把检查项串成一次运行,产出四层报告。 | |
| 流程:: | |
| collect.gather_evidence() 读批次目录 → Evidence | |
| │ | |
| ▼ | |
| applicable_checks() 按实际信道/模态选检查项 | |
| │ | |
| ▼ | |
| 逐项 run(ctx) 产出 Finding | |
| │ | |
| ▼ | |
| rollup 成 StreamResult 每条流一个结论 | |
| │ | |
| ▼ | |
| report.build_report() 四层结果 | |
| **只出结论,不产出数据**:不写文件、不改 episode 状态、不做任何清理动作。 | |
| 报告由调用方决定怎么用(落盘 / 展示 / 过滤导出)。 | |
| 设计取舍 | |
| -------- | |
| * **检查项失败不拖垮整轮** —— 单项抛异常记成 ``ERROR`` 结论继续跑, | |
| 否则一个 bug 会让整集没有报告。 | |
| * **不设 `on_failure`** —— 按用户要求简化:清洗只记录,不碰 episode 状态流转。 | |
| 需要拦截时由调用方读报告的 ``passed`` / ``verdict`` 自行决定。 | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import time | |
| from pathlib import Path | |
| from typing import Any, Iterable | |
| from app.processing.cleaning import collect | |
| from app.processing.cleaning.checks import ( | |
| CheckContext, applicable_checks, all_checks, | |
| ) | |
| from app.processing.cleaning.contract import ( | |
| ERROR, PASS, SEVERITY_LABELS, StreamResult, worst, | |
| ) | |
| from app.processing.cleaning.report import build_report, empty_report | |
| def ruleset_revision() -> str: | |
| """规则集指纹 —— 任何检查项的 slug/version 变了它就变。 | |
| 用途:报告里带上这个值,规则更新后能识别出"这份报告是用旧规则跑的"。 | |
| 与 ``workflow_dispatch.workflow_revision()`` 同款思路 —— 用内容哈希而不是 | |
| 自增版本号,省掉版本分配和迁移。 | |
| """ | |
| parts = sorted(f"{check.slug}@{check.version}" for check in all_checks()) | |
| return hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()[:24] | |
| def resolve_config(evidence: collect.Evidence, | |
| config: dict | None) -> dict[str, dict]: | |
| """把节点配置解析成 ``{检查项 slug: {"enabled": bool, "params": {...}}}``。 | |
| 节点配置结构(见 data_quality 节点):: | |
| { | |
| "devices": { | |
| "gripper_device": { | |
| "checks": { | |
| "umi.slam_continuity": {"enabled": true, | |
| "params": {"max_linear_mps": 3.0}}, | |
| "video.freeze": {"enabled": false} | |
| } | |
| } | |
| } | |
| } | |
| 本 episode 有多个设备卡片时,按 ``evidence.modalities`` 的顺序合并; | |
| **先出现的优先**(后面的不覆盖前面已设的值)。实际采集里一个 episode | |
| 基本只对应一张卡片,多卡片是罕见情况,规则简单可预期即可。 | |
| 没配到的检查项走默认:启用、用检查项自己声明的 ``default_params``。 | |
| """ | |
| resolved: dict[str, dict] = {} | |
| devices = (config or {}).get("devices") or {} | |
| for modality in evidence.modalities: | |
| card = devices.get(str(modality)) or {} | |
| for slug, item in (card.get("checks") or {}).items(): | |
| if slug in resolved: | |
| continue # 先出现的优先 | |
| entry = item if isinstance(item, dict) else {} | |
| resolved[slug] = { | |
| "enabled": bool(entry.get("enabled", True)), | |
| "params": dict(entry.get("params") or {}), | |
| } | |
| return resolved | |
| def _context_for(evidence: collect.Evidence, cfg: dict[str, dict]) -> CheckContext: | |
| """按证据构造检查上下文。阈值按"当前检查项"逐个注入。""" | |
| return CheckContext( | |
| batch_dir=evidence.batch_dir, | |
| channels=evidence.channels, | |
| modalities=evidence.modalities, | |
| data_rows=evidence.rows, | |
| data_columns=evidence.columns, | |
| duration_sec=evidence.duration_sec, | |
| fps=evidence.fps, | |
| streams=evidence.streams, | |
| ) | |
| def _stream_results(evidence: collect.Evidence, | |
| findings: Iterable) -> list[StreamResult]: | |
| """把 findings 归拢成"每条流一个结论" —— 报告的第一层。""" | |
| by_stream: dict[str, list] = {} | |
| for item in findings: | |
| by_stream.setdefault(item.stream or "", []).append(item) | |
| results: list[StreamResult] = [] | |
| seen: set[str] = set() | |
| for stream in evidence.streams: | |
| items = by_stream.get(stream.key) or [] | |
| seen.add(stream.key) | |
| status = worst(item.status for item in items) if items else PASS | |
| results.append(StreamResult( | |
| stream=stream.key, | |
| modality=",".join(evidence.modalities), | |
| label=stream.key, | |
| channels=(stream.channel,) if stream.channel else (), | |
| status=status, | |
| reasons=tuple(item.rule for item in items if item.status != PASS), | |
| metrics={k: v for item in items | |
| for k, v in (item.metrics or {}).items() | |
| if isinstance(v, (int, float, str, bool))}, | |
| ranges=tuple(r for item in items for r in (item.ranges or ())), | |
| )) | |
| # 没有对应证据流的 finding(如整集级结论)也要留在第一层里 | |
| for key, items in by_stream.items(): | |
| if key in seen: | |
| continue | |
| results.append(StreamResult( | |
| stream=key or "episode", | |
| status=worst(item.status for item in items), | |
| reasons=tuple(item.rule for item in items if item.status != PASS), | |
| ranges=tuple(r for item in items for r in (item.ranges or ())), | |
| )) | |
| return results | |
| def run_checks(batch_dir: Path, *, | |
| episode_id: str = "", | |
| episode_index: int | None = None, | |
| project: str = "", | |
| config: dict | None = None, | |
| probe_video: bool = False, | |
| modality_hint: str | Iterable[str] | None = None) -> dict: | |
| """跑一个 episode 的全部适用检查,返回四层报告(纯 dict,不含副作用)。 | |
| ``modality_hint`` 是设备模态(可多个,来自工作流连线的设备卡片)。给了它, | |
| ``resolve_config`` 才能查到用户在设置面板里按设备卡片配的阈值;给不出就 | |
| 走数据推断(推断不出 ``stereo_*``,那些 tab 的配置会被静默忽略)。 | |
| ``episode_index`` 只在【一个文件装多集】的布局下需要(LeRobot v3.0 导出 | |
| 产物就是这种);canonical 布局一集一文件,留空即可。**传错会导致把整份 | |
| 文件当成一集读**,所有指标因此失真 —— 而 ``localstore.get_episode()`` | |
| 返回的 ``path`` 是**项目目录**(多集共享),所以工作流路径**必须**带上它。 | |
| ``probe_video=False``(默认)只读 parquet,毫秒级;视频检查要解码抽样, | |
| 每集约几秒到几十秒,按需开。 | |
| """ | |
| started = time.perf_counter() | |
| evidence = collect.gather_evidence( | |
| batch_dir, episode_index=episode_index, | |
| probe_video=probe_video, modality_hint=modality_hint) | |
| if not evidence.streams: | |
| return empty_report( | |
| episode_id=episode_id, project=project, | |
| reason=str(evidence.notes.get("reason") or "no_data")) | |
| resolved = resolve_config(evidence, config) | |
| ctx = _context_for(evidence, resolved) | |
| # ★ 配置是【黑名单】,**不是白名单** —— 不传 ``enabled``。 | |
| # | |
| # ``applicable_checks`` 的 ``enabled`` 是白名单语义(``slug not in allow`` | |
| # 就跳过)。而前端设置面板只在用户**碰过**的检查项上写 entry(见 | |
| # DeviceQualityModal 的 setEntry,未触碰的项不落进 config),所以 | |
| # ``resolved`` 里通常只有一两条。把它当白名单 = 「用户在弹窗里改了一个 | |
| # 阈值 → 其余检查项全部静默停跑」:实测只配 umi.gripper_range 时, | |
| # 9 项变 1 项,且不报任何错。 | |
| # | |
| # 检查项没有"默认关闭"的概念(CleaningCheck 无 default_enabled), | |
| # 适用即跑;配置唯一的语义就是**关掉某项**。 | |
| # | |
| # (曾经写成 `{...enabled} or None`:那个 ``or None`` 只修了空集那半边 | |
| # ——"空集=一个都不跑"——非空的白名单照样是错的。整条白名单都不该有。) | |
| deny = {slug for slug, item in resolved.items() if not item["enabled"]} or None | |
| checks = applicable_checks( | |
| channels=evidence.channels, | |
| modalities=evidence.modalities, | |
| cross_modal_active=len(set(evidence.modalities)) >= 2 | |
| or len({item.channel for item in evidence.streams}) >= 2, | |
| disabled=deny, | |
| ) | |
| findings: list = [] | |
| failed_checks: list[str] = [] | |
| for check in checks: | |
| entry = resolved.get(check.slug) or {} | |
| ctx.params = dict(entry.get("params") or {}) | |
| try: | |
| findings.extend(check.run(ctx) or []) | |
| except Exception as exc: | |
| failed_checks.append(f"{check.slug}: {exc}") | |
| findings.append(_error_finding(check, exc)) | |
| # 视频流登记了却没检查 —— 必须显式记一笔,但**不能混进 findings**。 | |
| # | |
| # 视频检查项"适用"(rgb 信道存在)不等于"查过":``probe_video=False`` 时 | |
| # 它们拿到的是空报告,内部直接 continue,一条结论都不产出。报告若就此收尾, | |
| # 会得出 ``passed=True 全部通过`` —— 而实际上视频根本没看。 | |
| # | |
| # ★ 但它不该混进 findings 的严重度排序:``PENDING`` 排在 ``WARN`` 之前, | |
| # 一条"视频未检查"会把真实的力漂移警告盖掉(实测 52 集全被标成"待人工 | |
| # 确认")。"覆盖度"和"数据有多坏"是两个维度 —— 分开报。 | |
| unchecked: list[dict] = [] | |
| if not probe_video: | |
| rgb_streams = [item for item in evidence.streams if item.channel == "rgb"] | |
| if rgb_streams: | |
| unchecked.append({ | |
| "channel": "rgb", | |
| "reason": "probe_video_disabled", | |
| "message": f"{len(rgb_streams)} 路视频未检查(本轮只读了 parquet)", | |
| "streams": [item.key for item in rgb_streams], | |
| }) | |
| elapsed_ms = int((time.perf_counter() - started) * 1000) | |
| report = build_report( | |
| _stream_results(evidence, findings), | |
| findings, | |
| episode_id=episode_id, | |
| project=project, | |
| modalities=_modality_summary(evidence, [c.slug for c in checks]), | |
| channels=evidence.channels, | |
| ruleset_revision=ruleset_revision(), | |
| template_source=modality_hint or "", | |
| elapsed_ms=elapsed_ms, | |
| ) | |
| report["unchecked"] = unchecked | |
| if not checks: | |
| # 一个检查项都没跑 —— 报告在说"没问题",而实际是"没查"。 | |
| # 与 ``empty_report`` 同一条原则:没有结论不等于结论合格(fail closed)。 | |
| report["no_checks_ran"] = True | |
| # ``episode`` 只反映【数据本身】的结论;``passed`` 额外要求没有未检查项。 | |
| # | |
| # 分开的理由:前者是"数据有多坏",后者是"我们看全了没有"。混在一起会让 | |
| # "视频没检查"盖过"力漂移",也会让"数据确实没问题但没看全"看起来像数据坏了。 | |
| if unchecked or not checks: | |
| report["passed"] = False | |
| if failed_checks: | |
| # 检查项本身出错不改判定 —— 但要让人看见,否则会误以为"这集没问题" | |
| report["check_errors"] = failed_checks | |
| return report | |
| def _modality_summary(evidence: collect.Evidence, | |
| checks_run: Iterable[str]) -> dict: | |
| """PDF §1 的「数据清单」—— 这台设备是什么、有哪些信道、**实际跑了**哪些检查。 | |
| ``checks_run`` 必须是【真正执行了】的检查项(``applicable_checks`` 的结果), | |
| 不是配置里写的那份。两者可能差很远:设备卡片闸门会把不属于本设备的检查项 | |
| 滤掉,而报告如果只报配置,就分不清"检查了没问题"和"压根没检查"。 | |
| """ | |
| return { | |
| "device_cards": list(evidence.modalities), | |
| "channels": sorted(evidence.channels), | |
| "rows": evidence.rows, | |
| "fps": evidence.fps, | |
| "parquet": evidence.parquet, | |
| "checks_run": sorted(checks_run), | |
| } | |
| def _error_finding(check, exc: Exception): | |
| """检查项抛异常时的占位结论。 | |
| 用 ``ERROR``(处理失败)而不是 ``FAIL`` —— 这是"没检查成", | |
| 不是"数据坏了"。混为一谈会让数据质量问题看起来比实际多。 | |
| """ | |
| from app.processing.cleaning.contract import Finding | |
| return Finding( | |
| rule=check.slug, version=check.version, status=ERROR, | |
| scope="episode", | |
| message=f"check failed to run: {type(exc).__name__}: {exc}", | |
| ) | |