diff --git a/.gitignore b/.gitignore index 047f286..3e99025 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ .DS_Store *.textClipping .pytest_cache/ +.superpowers/ .venv/ dist/ build/ diff --git a/README_zh.md b/README_zh.md index 983ff4c..53e4927 100644 --- a/README_zh.md +++ b/README_zh.md @@ -20,7 +20,9 @@ ## 当前实现范围 -当前版本先实现纯业务状态机,不依赖摄像头模型。后续视觉模块只需要输出标准观察数据: +当前版本已经接入可运行的轻量视觉流程:区域占用、垃圾桶动作和 v1.2 的轻量 motion trajectory 都使用启发式图像差分实现,不使用 YOLO。后续训练好的 YOLO 食品检测模型会通过统一的 `disposal_evidence` / backend 合约接入,不改变批次计时状态机的业务输入形态。 + +视觉或 backend 模块需要输出标准观察数据: ```json { @@ -29,7 +31,23 @@ "1": 1, "2": 0 }, - "trash_deposit": false + "trash_deposit": false, + "disposal_evidence": [ + { + "source_zone_id": "1", + "target": "trash", + "confidence": 0.9, + "method": "motion", + "track_points": [ + {"x": 0.22, "y": 0.30}, + {"x": 0.48, "y": 0.58}, + {"x": 0.76, "y": 0.78} + ], + "item_class": null, + "detector_score": null, + "observed_at": "2026-04-27T10:00:03+08:00" + } + ] } ``` @@ -132,15 +150,17 @@ scripts/run_runtime.sh 2. 用 `ffmpeg` 周期抓取小尺寸 RGB 帧。 3. 按标定区域做占用变化检测。 4. 判断垃圾桶区域是否有明显投放动作。 -5. 调用批次计时状态机。 -6. 写入 `logs/events.jsonl`,管理页会读取这个文件。 +5. 对刚清空的来源区域运行轻量 motion trajectory,生成可选的 `disposal_evidence`。 +6. 调用批次计时状态机,优先使用匹配 `source_zone_id` 的 `disposal_evidence` 确认丢弃,再回退到通用垃圾桶动作。 +7. 写入 `logs/events.jsonl`,管理页会读取这个文件。 当前视觉版本是可运行的启发式版本: - 每个格口输出 `0/1` 占用状态,不识别单份数量。 - 启动后的前几帧用于建立空柜基线,默认 `3` 帧。 - 如果启动时格口里已经有食品,系统会把它当作基线,后续要等画面变化后才会产生计时事件。 -- 真实生产精度后续应接食品检测模型。 +- v1.2 轨迹识别是轻量 motion trajectory,不加载 YOLO,不要求模型文件。 +- 训练好的 YOLO 模型后续应作为新的 backend 接入,并继续输出统一的 `disposal_evidence`。 可选运行参数可以放在配置文件的 `[runtime]` 中: @@ -167,9 +187,29 @@ trash_motion_delta = 18.0 trash_sustained_motion_delta = 8.0 trash_sustained_motion_frames = 2 trash_motion_cooldown_seconds = 3 +trajectory_enabled = true +trajectory_window_seconds = 8 +trajectory_sample_interval_seconds = 1.0 +trajectory_min_points = 3 +trajectory_min_confidence = 0.72 +trajectory_motion_delta = 20.0 +trajectory_min_blob_area = 12 +trajectory_max_blob_area_fraction = 0.35 +trajectory_trash_entry_margin = 0.04 +trajectory_backend = "motion" +yolo_enabled = false +yolo_model_path = "" +yolo_min_confidence = 0.65 diagnostics_path = "logs/runtime_diagnostics.jsonl" ``` +`trajectory_backend = "motion"` 表示当前使用轻量轨迹 backend。`yolo_enabled`、`yolo_model_path` 和 `yolo_min_confidence` 是为后续训练模型预留的配置项;当前版本即使保留这些字段,也不会启用 YOLO 推理。 + +运行诊断写入 `logs/runtime_diagnostics.jsonl`。每行包含顶层 `disposal_evidence`,以及 `diagnostics.trajectory`: + +- 顶层 `disposal_evidence`:本帧实际输出给状态机的来源区域到垃圾桶证据。 +- `diagnostics.trajectory`:轻量轨迹 backend 的候选、过期、拒绝和已发出证据等调试信息。 + ## 本地测试 ```bash diff --git a/docs/project.md b/docs/project.md index 815edb6..2699bf5 100644 --- a/docs/project.md +++ b/docs/project.md @@ -17,7 +17,7 @@ The `v1.2 轨迹识别` batch adds source-zone trajectory evidence for disposal - `manage_api.py`: standard-library HTTP management API. - `main.py`: RTSP runtime loop connecting frame capture, vision, state engine, and JSONL sinks. - `vision.py`: heuristic ROI occupancy and trash-motion detection. - - v1.2 adds trajectory evidence between vision and engine: `MotionTrajectoryBackend` emits source-zone-to-trash evidence; `BatchEngine` consumes the backend-neutral `disposal_evidence` contract. + - v1.2 adds trajectory evidence between vision and engine: `TrajectoryTracker` emits source-zone-to-trash evidence; `BatchEngine` consumes the backend-neutral `disposal_evidence` contract. - Frontend package: `web/` - Vite + vanilla JavaScript management console. - Default web port `23000`. @@ -26,6 +26,7 @@ The `v1.2 轨迹识别` batch adds source-zone trajectory evidence for disposal - Runtime data: - Events JSONL default path `logs/events.jsonl`. - Diagnostics JSONL default path `logs/runtime_diagnostics.jsonl`. + - v1.2 diagnostics include root-level `disposal_evidence` plus `diagnostics.trajectory`. - Deployment: - Root Python Docker image for API/runtime. - `web/Dockerfile` for static web console. @@ -52,9 +53,34 @@ The `v1.2 轨迹识别` batch adds source-zone trajectory evidence for disposal - `trajectory_enabled`: enables source-zone trajectory evidence. - `trajectory_window_seconds`: seconds after a zone clears where movement can confirm disposal. - `trajectory_sample_interval_seconds`: faster runtime delay while a candidate is active. + - `trajectory_min_points`: minimum sampled motion points required before evidence can emit. - `trajectory_min_confidence`: minimum confidence before evidence can close pending disposal. + - `trajectory_motion_delta`: frame-difference threshold for trajectory motion points. + - `trajectory_min_blob_area`: minimum connected motion area to keep as a point. + - `trajectory_max_blob_area_fraction`: rejects overly broad frame motion as ambiguous. + - `trajectory_trash_entry_margin`: margin for treating a track point as entering the trash ROI. - `trajectory_backend`: first valid value is `"motion"`. - - `yolo_enabled`: defaults to `false`; reserved for a future trained model backend. + - `yolo_enabled`, `yolo_model_path`, `yolo_min_confidence`: reserved for a future trained model backend. Current v1.2 keeps YOLO disabled. + +## v1.2 Runtime Flow + +1. `RTSPFrameSource` captures a resized RGB frame. +2. `ZoneOccupancyDetector` updates per-zone binary occupancy and generic trash-motion count from calibrated ROIs. +3. `TrajectoryTracker` watches zones that just cleared, follows lightweight motion points toward the trash ROI, and emits source-specific `DisposalEvidence` when confidence passes the configured threshold. +4. `BatchEngine` processes `Observation(zone_counts, trash_deposit_count, disposal_evidence)`. +5. For pending disposal, matching `disposal_evidence.source_zone_id` confirms `batch_discarded` before generic FIFO `trash_deposit_count` fallback is used. +6. Runtime writes events to `logs/events.jsonl` and diagnostics to `logs/runtime_diagnostics.jsonl`. + +The current tracker is a motion backend only. A later trained YOLO detector should plug in as another backend that enriches or replaces the evidence producer while preserving the same `disposal_evidence` contract consumed by the engine. + +## Diagnostics + +- Runtime diagnostics JSONL records one item per runtime iteration. +- Root `disposal_evidence` is the exact evidence list passed into the engine for that iteration. +- `diagnostics.zones` contains occupancy metrics used to derive `zone_counts`. +- `diagnostics.trash` contains generic trash-motion metrics and cooldown state. +- `diagnostics.trajectory` contains v1.2 candidate counts, emitted evidence count, motion point count, and per-candidate emitted/rejected/expired records. +- Capture failures still keep the v1.2 schema with root `disposal_evidence: []` and `diagnostics.trajectory.reason = "frame_capture_failed"`. ## Event Model @@ -87,11 +113,17 @@ In v1.2, `batch_discarded` can be triggered by zone-scoped `disposal_evidence` b - `scripts/run_runtime.sh` - One-frame smoke test when camera and `ffmpeg` are available: - `PYTHONPATH=src python3 -m cold_display_guard.main --config config/example.toml --once` +- v1.2 operating notes: + - Keep `trajectory_backend = "motion"` and `yolo_enabled = false` unless a trained YOLO backend has been explicitly deployed. + - Confirm `logs/runtime_diagnostics.jsonl` contains top-level `disposal_evidence` and `diagnostics.trajectory` before judging trajectory behavior from events alone. + - When `TrajectoryTracker` has active candidates, runtime sampling uses `trajectory_sample_interval_seconds`; this can temporarily be faster than the normal `sample_interval_seconds`. + - On remote deployments, preserve the remote `config/example.toml` calibration and stream settings when syncing code. ## Known Risks - The current vision detector is heuristic and reports binary occupancy, not item counts. -- v1.2 motion tracking improves disposal matching but can still miss movement if the hand/object path is occluded or sampled too sparsely. +- v1.2 motion tracking improves disposal matching but can still miss movement if the hand/object path is occluded, too broad, too small, or sampled too sparsely. +- YOLO config fields are present for compatibility, but no trained YOLO model is part of the current runtime. - If food is already present during baseline collection, those regions may be treated as empty baseline until visual changes occur. - Changing calibration while the runtime process has active batches can create operational ambiguity; v1.1 should document or enforce a pause/restart expectation. - Historical events must keep the zone index at the time of emission so later region reordering does not reinterpret old logs. diff --git a/findings.md b/findings.md index a76a45f..733843c 100644 --- a/findings.md +++ b/findings.md @@ -77,6 +77,12 @@ Use `阶段 x` only as a workflow stage inside the same `v1.1 优化改造` batc - 轨迹诊断必须记录 active、emitted、rejected、expired,方便现场调参。 - 后续 YOLO 模型只作为 evidence 增强输入,不能修改 `BatchEngine` 的业务语义。 +## Final Review Findings + +- Evidence and generic trash fallback must share the same count budget: one matched `disposal_evidence` consumes one observed trash deposit signal when `trash_deposit_count` is also present, but extra trash deposits must still fall back to pending batches. +- A trajectory candidate must not append source-zone-external motion before source motion has been seen. If outside motion appears first and is later followed by source noise, the candidate is rejected with `motion_started_outside_source` instead of producing evidence. +- Runtime diagnostics on the deployed host should be checked for schema only, not by printing config, because the remote config may contain RTSP credentials and calibration. + ## Backend Planning Notes - `EngineSettings.zone_ids` should remain config driven; numeric zones are preferred for new configs, but old `r1c1` style IDs should continue loading. diff --git a/memories.md b/memories.md index 12152a7..b14944f 100644 --- a/memories.md +++ b/memories.md @@ -21,4 +21,19 @@ - Preserve a stable `disposal_evidence` contract for a future trained YOLO product detector. - Keep ROI occupancy timing as the source of zone occupied/empty state. - Use trajectory evidence before generic trash-motion FIFO fallback. -- Remote deployment target is `xiaozheng@192.168.5.206:/home/xiaozheng/cold_display_guard`; preserve remote `config/example.toml`. +- Current runtime writes top-level `disposal_evidence` and nested `diagnostics.trajectory` into runtime diagnostics JSONL. + +## v1.2 Completed Facts + +- Stage 1 established the backend contract: `Observation.disposal_evidence` normalizes backend-neutral disposal evidence, and the engine can discard a pending batch only when evidence targets `trash`, meets confidence, and matches the pending `source_zone_id`. +- Stage 2 added the lightweight motion trajectory runtime path: ROI occupancy still drives occupied/empty state, `TrajectoryTracker` emits source-zone-to-trash evidence, and generic trash-motion count remains as a fallback. +- Stage 3 added diagnostics and tests for runtime evidence propagation, trajectory sampling interval behavior, capture-failure schema, and trajectory/yolo runtime config parsing. +- Final review fixes: matched evidence now only subtracts the trash fallback budget by the number of batches it actually closed, and trajectory candidates reject outside-before-source motion with `motion_started_outside_source`. +- Current v1.2 does not use YOLO. `yolo_enabled`, `yolo_model_path`, and `yolo_min_confidence` are reserved for a future trained model backend that should keep emitting the same `disposal_evidence` shape. + +## Remote Deployment Notes + +- Remote deployment target is `xiaozheng@192.168.5.206:/home/xiaozheng/cold_display_guard`. +- Preserve the remote `config/example.toml`; it may contain camera, calibration, threshold, and deployment-specific runtime settings that must not be overwritten blindly. +- When syncing code remotely, verify that runtime diagnostics still show top-level `disposal_evidence` and `diagnostics.trajectory` before evaluating v1.2 trajectory behavior from `logs/events.jsonl`. +- The latest v1.2 deployment was verified with `cold-display-guard-runtime` and `cold-display-guard-api` up, API health `status=ok`, and diagnostics schema showing `has_disposal_evidence=True` plus `has_trajectory=True`. diff --git a/progress.md b/progress.md index f35bbe0..ab4221d 100644 --- a/progress.md +++ b/progress.md @@ -211,6 +211,16 @@ | 2026-05-29 | Phase 3 | Coding Agent | Fixed runtime integration review concerns | Error diagnostics keep trajectory schema; tests no longer use `create=True`; evidence payload helper type narrowed | | 2026-05-29 | Phase 3 | Testing Agent | Re-tested runtime integration concerns | Verdict pass; no new issues | | 2026-05-29 | Phase 3 | Main Agent | Ran local verification | `tests.test_main tests.test_vision` passed with 26 tests; full Python suite passed with 68 tests; dependency scan had no model/heavy vision matches | +| 2026-05-29 | Phase 4 | Main Agent | Marked Phase 4 as `in_progress` | Preparing documentation, final verification, remote deployment, and final review | +| 2026-05-29 | Phase 4 | Main Agent | Synced v1.2 code to `xiaozheng@192.168.5.206` | `rsync` completed while excluding remote `config/example.toml` | +| 2026-05-29 | Phase 4 | Main Agent | Tried remote config patch with inline heredoc | Failed with shell quoting `SyntaxError`; switching to scp temporary patch script | +| 2026-05-29 | Phase 4 | Main Agent | Patched remote runtime config via uploaded Python script | Added missing trajectory/yolo runtime keys without printing RTSP config | +| 2026-05-29 | Phase 4 | Main Agent | Rebuilt and restarted remote runtime/API containers | `cold-display-guard-runtime` and `cold-display-guard-api` recreated and started | +| 2026-05-29 | Phase 4 | Final Code Review Agent | Reviewed full v1.2 implementation | Verdict fail: extra trash fallback was suppressed, tracker could seed from outside-source motion, and docs named a non-existent backend | +| 2026-05-29 | Phase 4 | Main Agent | Fixed final review findings | Added regression tests, adjusted trash fallback budgeting, rejected outside-before-source trajectories, and renamed docs to `TrajectoryTracker` | +| 2026-05-29 | Phase 4 | Main Agent | Re-ran local full verification | Python 70 tests passed; frontend 22 tests passed; Vite build passed; no heavy model dependency matches | +| 2026-05-29 | Phase 4 | Main Agent | Re-synced and redeployed fix to `xiaozheng@192.168.5.206` | Runtime/API rebuilt and restarted from the fixed code | +| 2026-05-29 | Phase 4 | Main Agent | Verified remote runtime after redeploy | Containers are up, API health returns `status=ok`, diagnostics contain `disposal_evidence` and `diagnostics.trajectory` | ### Test Results @@ -224,6 +234,13 @@ | 2026-05-29 | `PYTHONPATH=src python3 -m unittest tests.test_main tests.test_vision -v` | pass | 26 runtime/vision tests passed after phase 3 | | 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 68 full Python tests passed after phase 3 | | 2026-05-29 | `rg -n "ultralytics|torch|onnxruntime|openvino|opencv|cv2|numpy" src tests pyproject.toml` | pass | No matches; command exited 1 because no heavy vision/model dependency was found | +| 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 70 full Python tests passed after final review fixes | +| 2026-05-29 | `node --test web/test/zone-state.test.js` | pass | 22 frontend model tests passed | +| 2026-05-29 | `cd web && pnpm build` | pass | Vite production build passed | +| 2026-05-29 | `rg -n "ultralytics|torch|onnxruntime|openvino|opencv|cv2|numpy" src tests pyproject.toml` | pass | No matches; command exited 1 because no heavy vision/model dependency was found | +| 2026-05-29 | `docker compose ... build cold-display-guard-runtime cold-display-guard-api && docker compose ... up -d --no-deps ...` on remote | pass | Runtime/API rebuilt and restarted after final fixes | +| 2026-05-29 | `curl --max-time 5 http://192.168.5.206:19080/api/manage/health` | pass | `status=ok`, `runtime_status=running` | +| 2026-05-29 | Remote diagnostics schema check script | pass | `has_disposal_evidence=True`, `has_trajectory=True` | ### Bug Loop @@ -238,6 +255,44 @@ | Phase 3 | Capture-error diagnostics rows lack `disposal_evidence` and `diagnostics.trajectory` schema | Added regression test and wrote empty evidence plus trajectory error diagnostics on capture failure | Resolved; testing agent and local full Python suite passed | | Phase 3 | Runtime tests patch `TrajectoryTracker` with `create=True`, which can mask missing imports | Removed `create=True` and asserted fake tracker observe calls | Resolved; testing agent and local full Python suite passed | | Phase 3 | `disposal_evidence_payloads()` accepts `list[object]` but blindly calls `asdict()` | Narrowed helper signature to `list[DisposalEvidence]` | Resolved; testing agent and local full Python suite passed | +| Phase 4 | Remote config patch inline heredoc lost the empty-string value for `yolo_model_path` | Switched to an scp-uploaded Python patch script instead of repeating inline quoting | Resolved; remote config patch reported 13 runtime keys patched | +| Phase 4 | Source-specific evidence disabled all generic fallback trash deposits | Subtract only the count of evidence-discard events from `remaining_trash_deposits`; add regression test for evidence plus `trash_deposit_count=2` | Resolved; targeted regression and full Python suite passed | +| Phase 4 | Tracker could keep an outside-source blob as the first point before source motion | Track outside-before-source contamination, reject later source noise with `motion_started_outside_source`, and never append those outside points | Resolved; targeted regression and full Python suite passed | +| Phase 4 | `docs/project.md` referenced non-existent `MotionTrajectoryBackend` | Changed architecture docs to name the implemented `TrajectoryTracker` | Resolved; `rg` no longer finds the stale name in project docs/README/src/tests | + +## 2026-05-29 Phase Completed: Phase 4 - Documentation, Verification, And Deployment + +Status: complete + +Files Changed: +- `.gitignore` +- `README_zh.md` +- `docs/project.md` +- `findings.md` +- `memories.md` +- `progress.md` +- `task_plan.md` +- `src/cold_display_guard/engine.py` +- `src/cold_display_guard/vision.py` +- `tests/test_engine.py` +- `tests/test_vision.py` + +Tests: +- `PYTHONPATH=src python3 -m unittest discover -s tests -v`: pass, 70 tests +- `node --test web/test/zone-state.test.js`: pass, 22 tests +- `cd web && pnpm build`: pass +- `rg -n "ultralytics|torch|onnxruntime|openvino|opencv|cv2|numpy" src tests pyproject.toml`: pass, no matches +- Remote Docker rebuild/restart: pass +- Remote API health: pass +- Remote diagnostics schema check: pass + +Notes: +- Final review findings were fixed before redeploy. +- Remote sync continued to exclude `config/example.toml` to preserve camera/calibration settings. +- Remote diagnostics check intentionally printed only schema booleans and safe trajectory keys. + +Risks: +- v1.2 is still heuristic motion tracking. Live precision should be tuned from diagnostics, and future YOLO integration should continue using the same `disposal_evidence` contract. ## 2026-05-29 Phase Completed: Phase 3 - Runtime Integration diff --git a/src/cold_display_guard/engine.py b/src/cold_display_guard/engine.py index 39f618d..1697c65 100644 --- a/src/cold_display_guard/engine.py +++ b/src/cold_display_guard/engine.py @@ -25,18 +25,15 @@ class BatchEngine: previous_zone_counts = dict(self._zone_counts) remaining_trash_deposits = observation.trash_deposit_count used_disposal_evidence: set[int] = set() - has_source_specific_disposal = self._has_confirming_disposal_evidence(observation.disposal_evidence) - if has_source_specific_disposal: - remaining_trash_deposits = 0 events.extend(self._expire_pending_disposal(observation.ts)) - events.extend( - self._apply_disposal_evidence( - observation.ts, - observation.disposal_evidence, - used_disposal_evidence, - ) + evidence_events = self._apply_disposal_evidence( + observation.ts, + observation.disposal_evidence, + used_disposal_evidence, ) + remaining_trash_deposits = max(0, remaining_trash_deposits - len(evidence_events)) + events.extend(evidence_events) trash_events = self._apply_trash_deposits(observation.ts, remaining_trash_deposits) remaining_trash_deposits = max(0, remaining_trash_deposits - len(trash_events)) events.extend(trash_events) @@ -82,13 +79,13 @@ class BatchEngine: self._zone_counts[zone_id] = new_count newly_pending_count = max(0, len(self.pending_disposal) - pending_count_before_zone_transitions) - events.extend( - self._apply_disposal_evidence( - observation.ts, - observation.disposal_evidence, - used_disposal_evidence, - ) + evidence_events = self._apply_disposal_evidence( + observation.ts, + observation.disposal_evidence, + used_disposal_evidence, ) + remaining_trash_deposits = max(0, remaining_trash_deposits - len(evidence_events)) + events.extend(evidence_events) trash_deposits_to_apply = remaining_trash_deposits if remaining_trash_deposits > 0 and newly_pending_count > 1: trash_deposits_to_apply = max(remaining_trash_deposits, newly_pending_count) @@ -283,9 +280,6 @@ class BatchEngine: events.append(self._event("batch_discarded", when, batch, severity="info")) return events - def _has_confirming_disposal_evidence(self, disposal_evidence: list[DisposalEvidence]) -> bool: - return any(self._is_confirming_disposal_evidence(evidence) for evidence in disposal_evidence) - def _is_confirming_disposal_evidence(self, evidence: DisposalEvidence) -> bool: if evidence.confidence < DISPOSAL_EVIDENCE_CONFIDENCE_THRESHOLD: return False diff --git a/src/cold_display_guard/vision.py b/src/cold_display_guard/vision.py index f566462..f30799b 100644 --- a/src/cold_display_guard/vision.py +++ b/src/cold_display_guard/vision.py @@ -83,6 +83,7 @@ class _TrajectoryCandidate: last_sample_at: datetime | None = None points: list[_MotionPoint] | None = None source_motion_seen: bool = False + pre_source_motion_seen: bool = False def __post_init__(self) -> None: if self.points is None: @@ -456,13 +457,22 @@ class TrajectoryTracker: available_source_blobs = [blob for blob in source_blobs if blob.blob_id not in consumed_blob_ids] if not available_source_blobs: return "ambiguous_motion_track" + if candidate.pre_source_motion_seen: + return "motion_started_outside_source" candidate.source_motion_seen = True point = _nearest_point(region_center(candidate.source_region), available_source_blobs) else: available_blobs = [blob for blob in blobs if blob.blob_id not in consumed_blob_ids] if not available_blobs: return None - point = _nearest_point(region_center(candidate.source_region), available_blobs) + candidate.pre_source_motion_seen = True + if any( + region_contains(self.trash_region, blob.x, blob.y, margin=self.settings.trajectory_trash_entry_margin) + for blob in available_blobs + if self.trash_region is not None + ): + return "motion_started_outside_source" + return None else: previous = candidate.points[-1] if candidate.points else None available_blobs = [blob for blob in blobs if blob.blob_id not in consumed_blob_ids] diff --git a/task_plan.md b/task_plan.md index a145ca0..b55ed47 100644 --- a/task_plan.md +++ b/task_plan.md @@ -85,12 +85,12 @@ Create and evolve an independent git project under `~/Code` for monitoring food ### Stop Conditions -- [ ] v1.2 所有阶段完成。 -- [ ] 必要 Python 测试通过。 -- [ ] 前端测试或构建在受影响时通过。 -- [ ] `docs/project.md` 记录 v1.2 架构、配置、运行方式和关键决策。 -- [ ] 没有 blocking bug 或未处理的高风险问题。 -- [ ] 如果同一问题连续 3 次修复仍失败,暂停并报告原因、已尝试方案和建议下一步。 +- [x] v1.2 所有阶段完成。 +- [x] 必要 Python 测试通过。 +- [x] 前端测试或构建在受影响时通过。 +- [x] `docs/project.md` 记录 v1.2 架构、配置、运行方式和关键决策。 +- [x] 没有 blocking bug 或未处理的高风险问题。 +- [x] 如果同一问题连续 3 次修复仍失败,暂停并报告原因、已尝试方案和建议下一步。 ### Phases @@ -99,7 +99,7 @@ Create and evolve an independent git project under `~/Code` for monitoring food | 1 | complete | 建立 `disposal_evidence` 数据契约并让状态机优先按来源区域丢弃 | `Observation` 支持 evidence;engine 能按 `source_zone_id` 精确关闭 pending batch;同帧移除+evidence 有回归测试;旧 `trash_deposit_count` 仍可兜底 | | 2 | complete | 实现无 YOLO 依赖的轻量轨迹检测 | synthetic frame 测试覆盖源区域到垃圾桶、非源区域运动、未到垃圾桶、单帧反光、多候选互不串扰;不引入模型依赖 | | 3 | complete | 集成 runtime 配置、诊断和候选窗口加速采样 | `main.py` 写入 `disposal_evidence` 与 trajectory diagnostics;配置默认 `trajectory_enabled=true`、`yolo_enabled=false`;候选活跃时使用更短采样间隔 | -| 4 | pending | 文档、全量验证和部署准备 | README/project/progress 更新;Python 全量测试通过;前端测试/构建按影响范围验证;远端部署命令和风险记录清楚 | +| 4 | complete | 文档、全量验证和部署准备 | README/project/progress 更新;Python 全量测试通过;前端测试/构建按影响范围验证;远端部署命令和风险记录清楚 | ### v1.2 Decisions diff --git a/tests/test_engine.py b/tests/test_engine.py index 525f2aa..e8a11ab 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -250,6 +250,33 @@ class BatchEngineTests(unittest.TestCase): self.assertEqual([(event["event"], event["zone_id"]) for event in events], [("batch_discarded", "4")]) self.assertEqual([batch.zone_id for batch in engine.pending_disposal], ["1"]) + def test_extra_trash_deposits_still_fallback_after_matching_disposal_evidence(self) -> None: + settings = EngineSettings( + camera_id="test_cam", + max_dwell_seconds=1200, + trash_confirmation_seconds=120, + zone_ids=("1", "2"), + ) + engine = BatchEngine(settings) + engine.process(obs(self.t0, {"1": 1, "2": 1})) + engine.process(obs(self.t0 + timedelta(seconds=1200), {"1": 1, "2": 1})) + engine.process(obs(self.t0 + timedelta(seconds=1300), {"1": 0, "2": 0})) + + events = engine.process( + obs( + self.t0 + timedelta(seconds=1310), + {"1": 0, "2": 0}, + trash=2, + disposal_evidence=[disposal_evidence("1")], + ) + ) + + self.assertEqual( + [(event["event"], event["zone_id"]) for event in events], + [("batch_discarded", "1"), ("batch_discarded", "2")], + ) + self.assertEqual(engine.pending_disposal, []) + def test_same_observation_removal_and_disposal_evidence_discards_newly_pending_batch(self) -> None: settings = EngineSettings( camera_id="test_cam", diff --git a/tests/test_vision.py b/tests/test_vision.py index c669668..e18a579 100644 --- a/tests/test_vision.py +++ b/tests/test_vision.py @@ -474,7 +474,39 @@ class VisionTests(unittest.TestCase): rejected.extend(diagnostics["rejected"]) self.assertEqual(all_evidence, []) - self.assertTrue(any(item["reason"] == "missing_source_motion" for item in rejected)) + self.assertTrue(any(item["reason"] == "motion_started_outside_source" for item in rejected)) + + def test_motion_before_source_motion_cannot_seed_later_trash_evidence(self) -> None: + source = Region("source", ((0.05, 0.35), (0.25, 0.35), (0.25, 0.65), (0.05, 0.65))) + trash = Region("trash", ((0.72, 0.35), (0.95, 0.35), (0.95, 0.65), (0.72, 0.65))) + tracker = TrajectoryTracker( + [source], + trash, + RuntimeVisionSettings(trajectory_sample_interval_seconds=0.0, trajectory_min_points=3), + ) + now = datetime(2026, 4, 28, 10, 0, tzinfo=timezone.utc) + + tracker.observe(solid_frame(80, 80, 40), now, {"source": 1}) + all_evidence = [] + rejected = [] + frames = [ + frame_with_motion_patch(80, 80, (34, 36)), + frame_with_motion_patch(80, 80, (16, 36)), + frame_with_motion_patch(80, 80, (34, 36)), + frame_with_motion_patch(80, 80, (50, 36)), + frame_with_motion_patch(80, 80, (66, 36)), + ] + for index, frame in enumerate(frames): + evidence, diagnostics = tracker.observe( + frame, + now + timedelta(seconds=index + 1), + {"source": 0}, + ) + all_evidence.extend(evidence) + rejected.extend(diagnostics["rejected"]) + + self.assertEqual(all_evidence, []) + self.assertTrue(any(item["reason"] == "motion_started_outside_source" for item in rejected)) def test_trajectory_diagnostics_include_per_candidate_events(self) -> None: source = Region("source", ((0.05, 0.35), (0.25, 0.35), (0.25, 0.65), (0.05, 0.65)))