Skip to content

Commit a660edb

Browse files
raullenchaiclaude
andcommitted
Honesty pass: reproducible forecast claims, latent interface, complete [all]
Address four credibility/honesty findings from a world-model researcher's usability test, keeping the small-&-beautiful tone and the legitimate encoder/world-model framing. - Forecast (卡点 5): drop the unreproducible "−35%" learned-vs-baseline number from the README hero caption and the forecast README (the training footage is not redistributed, only the checkpoint is). Reframe quick_forecast.py as the reproducible no-footage demo (it prints honest baseline-vs-baseline numbers from committed code) and clearly mark the real-video result as needing the user's own clip, with the export → train → render path documented. - Latent channel (卡点 6): reconcile the README's present-tense latent claims with DESIGN.md — the latent vec is a shipped, serializable interface; the V-JEPA/ReID producers are roadmap. Add examples/latent_vec.py demonstrating the channel is usable today by attaching your own embedding (track.user["vec"] → WorldState entity.vec → serialize → round-trip), numpy-only. - [all] extra (卡点 7): include the grounding deps (transformers/torch/pillow) so [all] genuinely installs every optional adapter, matching the README. - Examples (卡点 8): note that examples ship with the source tree (git clone), not the installed wheel. ruff clean; 92 passed / 1 skipped; both demos verified numpy-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3a5ca09 commit a660edb

5 files changed

Lines changed: 158 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
88

99
### Added
1010

11+
- Example `examples/latent_vec.py`: populate the dual-state latent `vec` channel by
12+
hand (attach your own embedding → `WorldState` entity → serialize → round-trip),
13+
runnable with numpy only — shows the latent interface is usable before the
14+
built-in producers ship.
1115
- `Detection.from_supervision(detections, class_names=None)`: ingest a Roboflow
1216
Supervision `sv.Detections` into `list[Detection]` by duck-typing (no
1317
`supervision` import), so Supervision users plug straight into Retina's event
@@ -21,6 +25,19 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2125
webhook) and `examples/from_supervision.py` (ingest a Roboflow `sv.Detections`
2226
pipeline), both runnable with no model / GPU / network on synthetic input.
2327

28+
### Changed
29+
30+
- `[all]` extra now installs every optional adapter's deps, including `grounding`
31+
(transformers + torch + pillow), so it is genuinely "everything" as the README says.
32+
- Honesty pass on forecast claims: dropped the unreproducible "−35%" learned-vs-baseline
33+
number from the README hero caption and the forecast README (the training footage
34+
isn't redistributed). `quick_forecast.py` is documented as the reproducible,
35+
no-footage demo; the real-video result is clearly marked as needing your own clip.
36+
- README latent-channel wording reconciled with `DESIGN.md`: the latent `vec` is a
37+
shipped, serializable *interface*; the automatic V-JEPA / ReID *producers* are
38+
roadmap, not shipped.
39+
- Noted that examples ship with the source tree (`git clone`), not the installed wheel.
40+
2441
## [0.1.0] — 2026-06-17
2542

2643
First public open-source release.

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Trio Retina
22

3-
**Turn any perception model's output into one standard, queryable world-state — symbolic events + latent vectors.**
3+
**Turn any perception model's output into one standard, queryable world-state — symbolic events, with a latent-vector channel built in.**
44
The model-agnostic state layer for world models.
55

66
*A lightweight, model-agnostic **computer-vision pipeline** for **object detection & tracking** that emits structured **events** — zone intrusion, line-crossing, dwell, people-counting — from **YOLO**, **VLM**, or **Grounding DINO** detectors over video, files, or **RTSP**. Runs on CPU at the **edge**; feeds **digital twins**, dynamics models, and LLMs.*
@@ -14,11 +14,11 @@ The model-agnostic state layer for world models.
1414

1515
![Trio Retina computer-vision pipeline: YOLO object tracking with two dynamics models forecasting entity trajectories from one world-state](https://raw.githubusercontent.com/machinefi/trio-retina/main/media/retina_demo.gif)
1616

17-
> One world-state from any detector → **two dynamics models forecast where each entity is headed** off the *same* state (gray = constant-velocity, magenta = learned, −35%). Swap the detector (YOLO → V-JEPA → DINO) or the dynamics model — the state in the middle is the constant.
17+
> One world-state from any detector → **two dynamics models forecast where each entity is headed** off the *same* state (gray = constant-velocity baseline, magenta = a learned model). Swap the detector (YOLO → V-JEPA → DINO) or the dynamics model — the state in the middle is the constant.
1818
1919
## 👋 hello
2020

21-
**Trio Retina** (Retina for short) turns raw signals — video, sensor — into a **queryable world-state**: readable **events** (`zone.enter`, `dwell`, `line.cross`) *plus* optional **latent** vectors, on one small model-agnostic standard. Bring any model (YOLO, V-JEPA, DINO, a VLM, or none); Retina assembles its output into state a dynamics model, rule engine, or LLM can consume.
21+
**Trio Retina** (Retina for short) turns raw signals — video, sensor — into a **queryable world-state**: readable **events** (`zone.enter`, `dwell`, `line.cross`) *plus* a standardized **latent** `vec` channel on the same records, on one small model-agnostic standard. The latent channel is a real, serializable interface today (attach your own embedding — see [`examples/latent_vec.py`](examples/latent_vec.py)); the automatic *producers* (V-JEPA scene + per-object ReID) are on the [roadmap](#-roadmap). Bring any model (YOLO, V-JEPA, DINO, a VLM, or none); Retina assembles its output into state a dynamics model, rule engine, or LLM can consume.
2222

2323
Think **OpenTelemetry for perception** — it doesn't build the sensors, it normalizes any of them into one state. In world-model terms it's the **encoder** (`s = Enc(x)`), and *only* the encoder; dynamics and policy build on top. → see [`DESIGN.md`](DESIGN.md).
2424

@@ -52,7 +52,7 @@ for event in cam.run(video_frames("dock.mp4")):
5252
# "label":"person","zone":"dock","dur":31.0,"conf":0.91}
5353
```
5454

55-
No model, no GPU? The [`examples/`](examples/) quickstarts run on synthetic detections — start with `python examples/quickstart.py` (the forecast / video demos need `[video]` + a clip).
55+
No model, no GPU? The [`examples/`](examples/) quickstarts run on synthetic detections — `git clone` the repo (they ship with the source, not the wheel) and start with `python examples/quickstart.py` (the forecast / video demos need `[video]` + a clip).
5656

5757
### compose models with `|`
5858

@@ -134,7 +134,7 @@ The hero GIF above. [`examples/forecast/`](examples/forecast/) runs a dynamics m
134134
<details>
135135
<summary>All examples</summary>
136136

137-
The top-level quickstarts run with **no model and no GPU** (synthetic detections):
137+
The examples live in this repo (not in the installed wheel) — `git clone` to run them. The top-level quickstarts run with **no model and no GPU** (synthetic detections):
138138

139139
```bash
140140
python examples/quickstart.py # zone / line / count / dwell events
@@ -144,6 +144,7 @@ python examples/gate_savings.py # a cheap gate cuts detector calls 100 ->
144144
python examples/pipeline_compose.py # compose with | (n8n without a GUI)
145145
python examples/rtsp_to_webhook.py # camera -> restricted-zone alert -> webhook
146146
python examples/from_supervision.py # ingest a Roboflow sv.Detections pipeline
147+
python examples/latent_vec.py # populate the latent vec channel by hand
147148
```
148149

149150
Real-footage / dynamics demos need a clip and the extras — `pip install 'trio-retina[all]'`:
@@ -189,7 +190,7 @@ Everything flows through one append-only data unit, the **`Frame`**. Each stage
189190

190191
**Two senses of "encoder."** Foundation backbones (V-JEPA, DINO, SAM, YOLO) turn pixels into features — that race is theirs, and Retina rides it. Retina is the encoder *layer* on top: it **fuses** many models into one record, gives objects **persistent identity**, **structures** it into entities + relations + events, carries the **dual** symbolic + latent channels, as an **event-sourced stream** — one small, serializable, model-agnostic standard.
191192

192-
**Dual state.** The same entities on two linked channels: *symbolic* (readable `events` / entity records, for rules / LLMs / dashboards) and *latent* (optional model-tagged embeddings, for a downstream dynamics model). Symbols you can read; vectors a model can predict on.
193+
**Dual state.** The same entities on two linked channels: *symbolic* (readable `events` / entity records, for rules / LLMs / dashboards) and *latent* (optional model-tagged embeddings, for a downstream dynamics model). Symbols you can read; vectors a model can predict on. The latent channel is a standardized, serializable interface shipping today — you can populate `entity.vec` with your own embedding now ([`examples/latent_vec.py`](examples/latent_vec.py)); the built-in *producers* (V-JEPA / ReID) are on the roadmap, not shipped yet.
193194

194195
**vs DeepStream / Holoscan** — same good ideas (event semantics, metadata model, composable graph), none of the weight:
195196

examples/forecast/README.md

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,42 @@ pixels, so Retina is the interface between any backbone and any forecaster.
99
behind one tiny `DynamicsModel` protocol (`observe` / `predict`):
1010

1111
- `LinearForecaster` — constant-velocity baseline (gray arrow).
12-
- `LearnedForecaster` — a small trained MLP (magenta arrow), **−35 %** centroid
13-
error vs the baseline on held-out entities.
12+
- `LearnedForecaster` — a small trained MLP (magenta arrow), which *can* anticipate
13+
turns / slow-downs a constant-velocity model misses.
1414

1515
Swap the detector (YOLO → V-JEPA → DINO) or the forecaster — the state in the
1616
middle is the constant.
1717

18-
## Run it
18+
## Run it — the reproducible demo (no model, no GPU, no footage)
19+
20+
```bash
21+
python examples/forecast/quick_forecast.py
22+
```
23+
24+
This is the runnable evidence: it builds a synthetic `WorldState` stream, forecasts
25+
a few frames ahead, and prints the real baseline-vs-baseline comparison — a
26+
constant-velocity model vs a no-motion model — so you can see the WorldState is
27+
genuinely dynamics-ready. Whatever the numbers are, they come straight from
28+
committed code.
1929

2030
```bash
21-
python examples/forecast/quick_forecast.py # synthetic — proves the loop, no model/GPU
22-
python examples/forecast/forecast_video.py v.mp4 # real-video constant-velocity baseline
2331
python examples/forecast/multi_consumer.py # one WorldState -> rules + forecast + an LLM-judge stub
2432
```
2533

26-
Reproduce the trained model and the annotated GIF:
34+
### The headline real-video result needs your own footage
35+
36+
The learned-vs-baseline comparison and the annotated GIF were measured on real
37+
traffic video, which we don't redistribute — so that specific result isn't
38+
reproducible from this repo alone (only the trained checkpoint is committed). A
39+
learned MLP does **not** automatically beat constant velocity; `train_dynamics.py`
40+
prints the honest win-or-tie on *your* data. To regenerate it end-to-end on a clip
41+
of your own:
2742

2843
```bash
29-
python examples/forecast/export_trajectories.py v.mp4 # WorldState tracks -> JSON
30-
python examples/forecast/train_dynamics.py traj.json dynamics.ckpt # train the MLP (torch)
31-
python examples/forecast/render_demo.py v.mp4 out.mp4 # the two-arrow demo video
44+
python examples/forecast/export_trajectories.py v.mp4 traj.json # WorldState tracks -> JSON ([video]+[yolo])
45+
python examples/forecast/train_dynamics.py traj.json dynamics.ckpt # train + report baseline vs learned (torch)
46+
python examples/forecast/forecast_video.py v.mp4 # real-video constant-velocity baseline
47+
python examples/forecast/render_demo.py v.mp4 out.mp4 # the two-arrow demo video
3248
```
3349

3450
## Files
@@ -40,7 +56,7 @@ python examples/forecast/render_demo.py v.mp4 out.mp4 # the two-arrow demo vid
4056
| `export_trajectories.py` | Run Retina (YOLO) → per-entity centroid tracks → JSON. |
4157
| `render_demo.py` | Render the annotated two-arrow demo video. |
4258
| `quick_forecast.py` · `forecast_video.py` · `multi_consumer.py` | Synthetic proof · real-video baseline · one-state-many-consumers. |
43-
| `dynamics.ckpt` | The trained MLP weights (committed, so the GIF is reproducible). |
59+
| `dynamics.ckpt` | The trained MLP weights from the demo footage (committed for the GIF; retrain on your own clip with `train_dynamics.py`). |
4460

4561
## What this is (and isn't)
4662

examples/latent_vec.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""The latent channel, populated by hand — runs with NO model and NO GPU.
2+
3+
Retina's state is **dual**: every entity carries a readable *symbolic* core AND an
4+
optional model-tagged *latent* `vec` on the *same* record (see DESIGN.md / SPEC.md
5+
"The latent channel"). The `Vec` type and the `entity.vec` slot are a real,
6+
serializable interface today — what's on the roadmap is the *producers* that fill
7+
it automatically (V-JEPA scene latents, per-object ReID). This example shows the
8+
interface is usable right now by attaching your **own** embedding:
9+
10+
track.user["vec"] = Vec(...).to_dict() # any embedding you already have
11+
→ flows into WorldState → entity.vec → serialize → round-trips
12+
13+
So if you have an embedder (a frozen backbone, a ReID head, anything), you can ride
14+
the latent channel today; you don't have to wait for the shipped producers.
15+
16+
python examples/latent_vec.py
17+
"""
18+
19+
import numpy as np
20+
21+
from retina import IoUTracker, Vec, WorldState
22+
from retina.detect import Detection
23+
from retina.nodes import DetectorNode, TrackerNode
24+
from retina.pipeline import Pipeline
25+
26+
27+
class Walker:
28+
"""One 'person' walking across the frame — one detection per call."""
29+
30+
def __init__(self):
31+
self.f = 0
32+
33+
def __call__(self, image):
34+
x, y = 60 + self.f * 8, 200 + self.f * 4
35+
self.f += 1
36+
return [Detection("person", (x - 25, y - 25, x + 25, y + 25), 0.9)]
37+
38+
39+
def my_embedding(track) -> Vec:
40+
"""Stand in for a real producer (ReID head / frozen V-JEPA ROI). Here we just
41+
hash the track's box into a tiny deterministic vector — the point is the
42+
*channel*, not the model. Swap this for your own embedder and nothing else
43+
changes."""
44+
x1, y1, x2, y2 = track.bbox
45+
rng = np.random.default_rng(int(x1 + y1) % 2**32)
46+
values = rng.standard_normal(8).round(3).tolist()
47+
return Vec(model="demo-reid/v0", dim=8, values=values)
48+
49+
50+
def main() -> None:
51+
pipe = Pipeline(
52+
[DetectorNode(Walker()), TrackerNode(IoUTracker(min_hits=2))], source_id="cam"
53+
)
54+
55+
entity = None
56+
for i in range(4):
57+
frame = pipe.process(np.zeros((400, 640, 3), np.uint8), float(i))
58+
# Attach YOUR embedding to each track's open `user` slot. `WorldState`
59+
# reads `track.user["vec"]` and carries it onto the entity automatically.
60+
for trk in frame.tracks:
61+
trk.user["vec"] = my_embedding(trk).to_dict()
62+
ws = WorldState.from_frame(frame)
63+
if ws.entities:
64+
entity = ws.entities[-1]
65+
66+
assert entity is not None, "tracker should have confirmed the walker"
67+
print("entity (symbolic core + latent vec):")
68+
print(f" id={entity.id} type={entity.type} bbox={tuple(round(v) for v in entity.bbox)}")
69+
print(f" vec.model={entity.vec.model} vec.dim={entity.vec.dim}")
70+
print(f" vec.values={entity.vec.values}\n")
71+
72+
# The dual state serializes as one record — symbol + latent, never collapsed —
73+
# and round-trips losslessly through JSON.
74+
blob = ws.to_json()
75+
print(f"serialized WorldState ({len(blob)} bytes):")
76+
print(f" {blob}\n")
77+
78+
rt = WorldState(**_load_entities(blob))
79+
e2 = rt.entities[-1]
80+
assert e2.vec is not None and e2.vec.values == entity.vec.values
81+
print("round-trip ok: the latent vec survived serialize → parse, attached to its entity.")
82+
83+
84+
def _load_entities(blob: str) -> dict:
85+
"""Rebuild a WorldState from its JSON, reconstructing nested Entity/Vec."""
86+
import json
87+
88+
from retina import Entity
89+
90+
d = json.loads(blob)
91+
ents = []
92+
for e in d.get("entities", []):
93+
vec = Vec(**e["vec"]) if "vec" in e else None
94+
ents.append(
95+
Entity(
96+
id=e["id"],
97+
type=e["type"],
98+
bbox=tuple(e["bbox"]) if "bbox" in e else None,
99+
conf=e.get("conf"),
100+
vec=vec,
101+
)
102+
)
103+
return {"src": d["src"], "t": d["t"], "frame": d.get("frame"), "entities": ents}
104+
105+
106+
if __name__ == "__main__":
107+
main()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ yolo = ["ultralytics>=8.3"]
3939
norfair = ["norfair>=2.2"]
4040
grounding = ["transformers>=4.40", "torch>=2.0", "pillow>=10"]
4141
video = ["opencv-python>=4.8"]
42-
all = ["ultralytics>=8.3", "norfair>=2.2", "opencv-python>=4.8"]
42+
all = ["ultralytics>=8.3", "norfair>=2.2", "opencv-python>=4.8", "transformers>=4.40", "torch>=2.0", "pillow>=10"]
4343
dev = ["pytest>=8", "ruff>=0.6", "norfair>=2.2"]
4444
docs = ["mkdocs-material>=9.5", "mkdocstrings[python]>=0.26"]
4545

0 commit comments

Comments
 (0)