Spaces:
Running
Running
stouchinglhh
Claude
sync processor from GitLab c697b6e: 版本 1.19.4 / 前端 1.12.4 —— 服务端 i18n(首屏不再闪英文)、推理模块并入模型页、前端抽出 shared/、缓存键改用前端号
bb59211 Download processor/app/processing/cleaning/report.py from Stouching-Robotics/DataFlowing: direct link, hf CLI and curl.
- Browser
- Download file 9.09 kB
-
https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/processor/app/processing/cleaning/report.py
- Command line
-
hf download hf://spaces/Stouching-Robotics/DataFlowing/processor/app/processing/cleaning/report.py
-
curl -L -o report.py https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/processor/app/processing/cleaning/report.py
9.09 kB
| """清洗报告组装 —— PDF §5 的「四层结果」。 | |
| 第一层 数据流 哪条流好、哪条流坏 | |
| 第二层 时间区间 问题发生在哪几秒、哪些帧 | |
| 第三层 episode 这一集整体什么结论 | |
| 第四层 训练用途 能用于什么训练、不能用于什么 | |
| 全部是纯函数 —— 输入是检查项产出的 ``StreamResult`` / ``Finding``, | |
| 输出是可 JSON 序列化的 dict。不碰磁盘、不碰 cv2,因此完全可单测。 | |
| """ | |
| from __future__ import annotations | |
| from datetime import datetime, timezone | |
| from typing import Any, Iterable | |
| from app.processing.cleaning.contract import ( | |
| Finding, Range, StreamResult, | |
| PASS, WARN, FAIL, PENDING, ERROR, | |
| SEVERITY_ORDER, SEVERITY_LABELS, worst, is_blocking, | |
| ) | |
| SCHEMA_VERSION = 2 | |
| # 「训练用途」判定表 —— 声明式,不是 if-else 堆出来的。 | |
| # requires 里的信道必须全部具备,该用途才成立。 | |
| # PDF §7 要求"说明 episode 适合纯视觉、触觉或联合训练",这张表就是那个说明。 | |
| TRAINING_USE_RULES: tuple[dict[str, Any], ...] = ( | |
| {"use": "pure_vision", "label": "Pure vision", | |
| "requires": ("rgb", "time")}, | |
| {"use": "vision_gripper_joint", "label": "Vision + gripper", | |
| "requires": ("rgb", "gripper_state", "time")}, | |
| {"use": "slam_trajectory", "label": "SLAM trajectory", | |
| "requires": ("slam", "time")}, | |
| {"use": "vision_tactile_joint", "label": "Vision + tactile", | |
| "requires": ("rgb", "tactile", "time")}, | |
| {"use": "bimanual_tactile", "label": "Bimanual tactile", | |
| "requires": ("tactile", "time")}, | |
| ) | |
| def _utcnow() -> str: | |
| return datetime.now(timezone.utc).isoformat() | |
| def build_timeline(streams: Iterable[StreamResult | dict]) -> list[dict]: | |
| """把各条流里的区间拍平成一条时间轴,按时间排序去重。 | |
| 详情页要一次拉完整条时间轴,而 streams 是嵌套的 —— 所以顶层冗余一份。 | |
| 单一来源、单一方向(从 streams 派生),有测试保证一致。 | |
| """ | |
| items: list[dict] = [] | |
| for stream in streams: | |
| if isinstance(stream, StreamResult): | |
| for item in stream.ranges: | |
| items.append(_range_row(item.to_dict(), stream.stream)) | |
| elif isinstance(stream, dict): | |
| for item in stream.get("ranges") or []: | |
| if isinstance(item, dict): | |
| items.append(_range_row(item, str(stream.get("stream") or ""))) | |
| items.sort(key=lambda row: (row["start_sec"], row["end_sec"], row["code"])) | |
| deduped: list[dict] = [] | |
| for row in items: | |
| if deduped and all( | |
| row[key] == deduped[-1][key] | |
| for key in ("start_sec", "end_sec", "code", "stream") | |
| ): | |
| continue | |
| deduped.append(row) | |
| return deduped | |
| def _range_row(item: dict, stream: str) -> dict: | |
| return { | |
| "stream": item.get("stream") or stream, | |
| "channel": item.get("channel") or "", | |
| "start_sec": float(item.get("start_sec") or 0.0), | |
| "end_sec": float(item.get("end_sec") or 0.0), | |
| "start_frame": item.get("start_frame"), | |
| "end_frame": item.get("end_frame"), | |
| "code": str(item.get("code") or ""), | |
| "severity": str(item.get("severity") or WARN), | |
| "rule": str(item.get("rule") or ""), | |
| "message": str(item.get("message") or ""), | |
| } | |
| def rollup_episode(streams: Iterable[StreamResult | dict], | |
| findings: Iterable[Finding | dict] = ()) -> dict: | |
| """第三层:整集结论。纯函数。 | |
| 取所有流 + 所有 finding 里最严重的那个状态。 | |
| """ | |
| statuses: list[str] = [] | |
| failed: list[str] = [] | |
| warned: list[str] = [] | |
| pending: list[str] = [] | |
| for stream in streams: | |
| if isinstance(stream, StreamResult): | |
| name, status = stream.stream, stream.status | |
| else: | |
| name, status = str(stream.get("stream") or ""), str(stream.get("status") or PASS) | |
| statuses.append(status) | |
| if is_blocking(status): | |
| failed.append(name) | |
| elif status == PENDING: | |
| pending.append(name) | |
| elif status == WARN: | |
| warned.append(name) | |
| for finding in findings: | |
| status = finding.status if isinstance(finding, Finding) else str(finding.get("status") or PASS) | |
| statuses.append(status) | |
| overall = worst(statuses) if statuses else PASS | |
| pieces: list[str] = [] | |
| if failed: | |
| pieces.append(f"{len(failed)} stream(s) failed") | |
| if warned: | |
| pieces.append(f"{len(warned)} stream(s) with warnings") | |
| if pending: | |
| pieces.append(f"{len(pending)} stream(s) pending review") | |
| return { | |
| "status": overall, | |
| "status_label": SEVERITY_LABELS.get(overall, overall), | |
| "failed_streams": failed, | |
| "warn_streams": warned, | |
| "pending_streams": pending, | |
| "summary": "; ".join(pieces) if pieces else "All checks passed", | |
| } | |
| def rollup_training_use(channels: Iterable[str], | |
| episode_status: str = PASS) -> list[dict]: | |
| """第四层:这个 episode 能用于哪些训练。纯函数。 | |
| 信道不具备 → 该用途判 fail("未采集"),这是**用途维度**的失败, | |
| 不代表数据本身坏 —— 所以 reason 写清楚是"未采集"而不是"质量差"。 | |
| """ | |
| have = {str(item) for item in channels} | |
| rows: list[dict] = [] | |
| for rule in TRAINING_USE_RULES: | |
| required = tuple(rule["requires"]) | |
| missing = [channel for channel in required if channel not in have] | |
| if missing: | |
| rows.append({ | |
| "use": rule["use"], | |
| "label": rule["label"], | |
| "status": FAIL, | |
| "reason": f"not collected: {', '.join(missing)}", | |
| "requires": list(required), | |
| "missing": missing, | |
| }) | |
| else: | |
| rows.append({ | |
| "use": rule["use"], | |
| "label": rule["label"], | |
| "status": episode_status, | |
| "reason": "" if episode_status == PASS else "data quality problem", | |
| "requires": list(required), | |
| "missing": [], | |
| }) | |
| return rows | |
| def passed_for(status: str) -> bool: | |
| """该状态是否算"通过"(只有 PASS 算通过,WARN 不算)。""" | |
| return status == PASS | |
| def build_report(streams: Iterable[StreamResult], | |
| findings: Iterable[Finding] = (), | |
| *, | |
| episode_id: str = "", | |
| project: str = "", | |
| modalities: dict[str, Any] | None = None, | |
| channels: Iterable[str] = (), | |
| ruleset_revision: str = "", | |
| template_source: str = "", | |
| elapsed_ms: int = 0) -> dict: | |
| """组装完整报告。纯函数 —— 唯一的副作用是 ``_utcnow()``。""" | |
| stream_list = list(streams) | |
| finding_list = list(findings) | |
| episode = rollup_episode(stream_list, finding_list) | |
| use_channels = {str(item) for item in channels} | |
| for stream in stream_list: | |
| use_channels.update(stream.channels) | |
| return { | |
| "schema_version": SCHEMA_VERSION, | |
| "episode_id": episode_id, | |
| "project": project, | |
| "computed_at": _utcnow(), | |
| "elapsed_ms": int(elapsed_ms), | |
| "ruleset_revision": ruleset_revision, | |
| "template_source": template_source, | |
| # 模态清单 —— PDF §1 的「数据清单」 | |
| "modalities": dict(modalities or {}), | |
| # 判定总入口:只有 PASS 算通过 | |
| "passed": passed_for(episode["status"]), | |
| # 四层结果 | |
| "streams": [ | |
| item.to_dict() if isinstance(item, StreamResult) else dict(item) | |
| for item in stream_list | |
| ], | |
| "ranges": build_timeline(stream_list), | |
| "episode": episode, | |
| "training_use": rollup_training_use(use_channels, episode["status"]), | |
| "findings": [ | |
| item.to_dict() if isinstance(item, Finding) else dict(item) | |
| for item in finding_list | |
| ], | |
| } | |
| def empty_report(*, episode_id: str = "", project: str = "", | |
| reason: str = "no_data") -> dict: | |
| """没有任何数据可检查时的占位报告。 | |
| ``passed`` 恒为 False —— 没有数据不等于数据合格(fail closed)。 | |
| """ | |
| return { | |
| "schema_version": SCHEMA_VERSION, | |
| "episode_id": episode_id, | |
| "project": project, | |
| "computed_at": _utcnow(), | |
| "elapsed_ms": 0, | |
| "ruleset_revision": "", | |
| "template_source": "", | |
| "modalities": {}, | |
| "passed": False, | |
| "streams": [], | |
| "ranges": [], | |
| "episode": { | |
| "status": ERROR, | |
| "status_label": SEVERITY_LABELS[ERROR], | |
| "failed_streams": [], | |
| "warn_streams": [], | |
| "pending_streams": [], | |
| "summary": reason, | |
| }, | |
| "training_use": [], | |
| "findings": [], | |
| "reason": reason, | |
| } | |