feat: reveal all hidden shapes with Shift+H, release SAM3 VRAM on idle and on demand (REQ-191, REQ-192)

This commit is contained in:
asus committed 2026-10-07 11:55:00 +07:00
1 parent f5be7880b0
commit fd608848bf
16 files changed
+290 -20

No files matched your search

+10 -2
View File
@@ -116,7 +116,7 @@ box.
| Module | Role | Status |
|---|---|---|
| `sam3_engine.py` | SAM3 singleton, `open_state`/`apply_prompts`/`segment_at` | reused, plus a `release()` for REQ-065 |
| `sam3_engine.py` | SAM3 singleton, `open_state`/`apply_prompts`/`segment_at` | reused, plus `release_engine()` for REQ-065 and the idle-unload watcher (REQ-192): a daemon thread, started on first `get_engine()`, that takes the non-blocking `gpu_lock` and releases after `SAM3_IDLE_UNLOAD_S` seconds without use |
| `labeling.py` | per-frame detection + cross-class greedy NMS with per-class IoU override and the container containment carve-out (REQ-031, REQ-184) | reused; the folder-walking half went with the old flow |
| `exporters.py` | ~~YOLO label writing~~ | **deleted** — `dataset.py` writes labels, `mask_to_polygons` moved to `review.py` |
| `sessions.py` | ~~exemplar/tap interaction~~ | **deleted** — see below |
@@ -158,6 +158,7 @@ namespaces stay distinguishable.
```
GET /api/health REQ-073
POST /api/sam3/release # free SAM3's VRAM now; 409 while the GPU is busy (REQ-192)
GET /api/projects REQ-001
POST /api/projects REQ-001,002,004,005
@@ -379,7 +380,7 @@ rejection. Parsing is `parseClassYaml` in the same file — a strict subset read
what Copy emits, no YAML dependency, all-or-nothing with a `line N: …` error. Because
`ClassParamsTable` is shared, the mass modal carries both buttons too.
**Review sidebar class rows (REQ-180, REQ-183, REQ-185).** Each class row's frame-clear `×`
**Review sidebar class rows (REQ-180, REQ-183, REQ-185, REQ-191).** Each class row's frame-clear `×`
(REQ-180) gains an eye toggle in front of it: session-only `Set` state in `ReviewPage`, so it
survives frame changes, resets when the review page is left, and never touches stored data.
Hidden shapes are never handed to `AnnotationCanvas` — not drawn, not clickable, not
@@ -393,6 +394,13 @@ keeps its real per-frame count. Creating a shape while its class is hidden (draw
copy) auto-overrides it so the user sees what they just made; reclassing into a hidden class
dims it unless it already carries an override; toggling a class eye clears that class's overrides.
A third, independent piece of state is `revealAll` (REQ-191): a boolean **overlay** in
`ReviewPage`, toggled by `Shift+H`, that makes `isShapeVisible` answer "yes" for every shape.
It reads neither Set and writes neither Set, which is what makes the second `Shift+H` restore
the previous hidden state exactly; a plain `H` while it is on only clears the overlay, so the
keystroke edits the real hidden state from then on. The sidebar shows a `revealing hidden` chip
in the "Shapes on this frame" header while it is on (REQ-191).
**Exemplar-driven labeling in review (REQ-173, REQ-174, REQ-175).** In `draw` mode a drag on
`AnnotationCanvas` is an exemplar, not a rectangle: `onExemplar(box, positive)` where
`positive` is `!event.shiftKey`. `hooks/useExemplarPool.js` keeps the pool in a ref as well
+17
View File
@@ -276,6 +276,13 @@ changes.
auto-overridden so the user sees what they just made. Like the class eye, this is session
state: it survives frame changes, resets when the review page is left, and never touches
stored data.
- **REQ-191** — `Shift+H` in the review editor toggles a **reveal-all overlay**: every hidden
shape — `H`-hidden shapes and shapes of hidden classes alike — is drawn on the canvas again
while nothing about what is actually hidden changes. A second `Shift+H` turns the overlay
off and the previous hidden state is back exactly as it was. While the overlay is on, the
"Shapes on this frame" panel shows a chip saying shapes are being revealed, and a plain `H`
drops an active overlay first instead of hiding anything, so from then on the keystroke
edits the real hidden state. Session-only, like the hide state itself.
## E4. Live counting preview
@@ -479,3 +486,13 @@ changes.
folder has moved once already and everything in it must survive the next move), which
strengthens the never-hardcoded rule of REQ-072. Training, preview, the batch job, model
download and live counting all reach the same weights through this one resolution.
- **REQ-192** — The SAM3 model's ~4 GB of VRAM is not held forever. After
`SAM3_IDLE_UNLOAD_S` seconds (default 1200 = 20 minutes; `0` never; an environment
variable, per REQ-072) without any SAM3 use, the model is released — through the same
release path training already uses (REQ-065) and under the same one-holder rule: a job or
an interactive assist in flight postpones the release to the next check, it is never
interrupted. `POST /api/sam3/release` frees it immediately instead of waiting, and answers
`409` while the GPU is busy. `/api/health`'s `model_loaded` field reports the state, and the
topbar badge shows `SAM3: Loaded` (green, clickable to release) / `SAM3: Idle` (not in VRAM,
loads on next use) / `SAM3: Off` (not enough free VRAM to load). The next call after a
release rebuilds the model from the local cache in about 12 seconds.
+53
View File
@@ -1565,6 +1565,59 @@ read it.
frame already holding an overlapping truck → banner `skipped K overlapping truck`, dot, count
unchanged, while a frame holding a truck elsewhere gets its own second box.
## Task — Reveal all hidden shapes with Shift+H (REQ-191) `[DONE]`
- `frontend/src/pages/ReviewPage.jsx` — third piece of hide state, `revealAll`, is a boolean
**overlay**: `isShapeVisible` answers yes for everything while it is on, and neither
`hiddenShapeIds` nor `hiddenClasses`/`overriddenShapeIds` is touched, which is what makes the
second `Shift+H` restore the previous hidden state exactly. In the keydown handler,
`Shift+H` toggles the overlay, a plain `H` clears an active overlay first (so it edits the
real hidden state) and otherwise falls through to `toggleHideSelected()`.
- `frontend/src/components/ReviewSidebar.jsx` — `revealing hidden` chip (SVG `EyeIcon`, accent
pill, `title` naming `Shift+H`) in the *Shapes on this frame* header while the overlay is on;
dimming of rows is unchanged, so the chip and the dims together say what is actually hidden.
- `frontend/src/components/ShortcutsPanel.jsx` — `Shift+H` row next to `H`.
- `frontend/src/app.css` — `.reveal-chip` on `--accent` tokens (works in both themes).
- Docs: `requirements.md` REQ-191, `design.md` sidebar block, `ui-spec.md` keyboard table +
shapes-panel bullet.
→ verify: **[DONE]** `npm run build` green; `git diff --check` clean. **Still needs a browser
pass:** hide a class and one shape, `Shift+H` → both drawn + chip up, `Shift+H` again → back
to the same dims, plain `H` → overlay drops and only then hides the selection.
## Task — Free SAM3's VRAM on idle and on demand (REQ-192) `[DONE]`
- Question answered first: nothing freed the model before — `release_engine()` had exactly one
caller (`backend/training.py`, REQ-065), so the ~3.9 GB stayed in the card until a training
run or the process exited. The YOLO loads elsewhere are transient locals, no leak found.
- `backend/sam3_engine.py` — `IDLE_UNLOAD_S` (env `SAM3_IDLE_UNLOAD_S`, default 1200, `0`
disables), `_touch()` stamped by `get_engine()` and by the four model methods that actually
run inference (so a long autolabel job counts as use, not as idle), and one daemon watcher
started on first `get_engine()`: every `min(60 s, IDLE_UNLOAD_S)` it releases the engine,
but only after taking `jobs.gpu_lock` **without blocking** — a job or assist in flight just
postpones the release, never gets interrupted (same one-holder rule as REQ-065).
- `backend/api/sam3.py` — `POST /api/sam3/release`: same non-blocking `gpu_lock`, `409` while
the GPU is busy, `{"released", "model_loaded"}` otherwise; wired in `backend/main.py`.
- `docker-compose.yml` + `.env.example` — the variable is passed through, so the container
sees a value set in `.env` (empty/unset → Python falls back to 1200).
- `frontend/src/components/Sidebar.jsx` + `api.js` + `roboflow.css` — badge is now three-state:
`SAM3: Loaded` (green **button**, click releases, optimistic flip, 3 s poll confirms) /
`SAM3: Idle` / `SAM3: Off`; a failed release shows its `detail` for 4 s instead of failing
silently.
- Docs: `requirements.md` REQ-192, `design.md` module row + API contract, `ui-spec.md` badge
block, `.env.example` GPU section.
→ verify: **[DONE]** `uv run python /tmp/opencode/test_idle_unload.py` — ALL PASS: unload after
idle, release postponed while `gpu_lock` held then fires, `0` disables the watcher, endpoint
answers `409` when busy and `{"released": true, "model_loaded": false}` when free; app imports
with `/api/sam3/release` in the OpenAPI paths; `npm run build` green; `git diff --check` clean.
**Live run against the rebuilt container:** assist on a batch frame loads the model (VRAM free
7.39 → 3.15 GB), `POST /api/sam3/release` answers `{"released": true, "model_loaded": false}`
and VRAM goes back to 7.32 GB (+4.2 GB freed), the next assist rebuilds in 6 s and works,
badge strings present in the served `index-BGbzE_yy.js`. Container env shows
`SAM3_IDLE_UNLOAD_S` empty → falls back to the 1200 s default as designed. **Watcher verified
live too:** with `SAM3_IDLE_UNLOAD_S=10` on one container run, a loaded model (3.15 GB free)
went to `model_loaded: false` / 7.32 GB free on its own within the 10 s tick, then the
container was put back on the default 1200 s.
## Known open points
- *Not closed by any task, by choice:* **any rebuild kills the running job.** Task 14's resume
+9 -3
View File
@@ -201,7 +201,9 @@ falling back to `route.projectId` and then `1`:
```
GPU: <name, stripped of "NVIDIA GeForce" / "Laptop GPU"> | CPU if none
VRAM: <free>GB | N/A
SAM3: Ready | Off ← green when ready
SAM3: Loaded | Idle | Off ← Loaded = green button, click frees the VRAM (REQ-192)
Idle = green, not in VRAM, loads on next use (~12 s)
Off = not enough free VRAM to load
```
Icons are SVG (Heroicons/Lucide-style), never emoji. *(Some emoji survive inside page bodies —
@@ -1171,6 +1173,7 @@ Handlers are attached in the **capture** phase on `document`, and:
| `1`–`9` | set active class; reclass the selected shape if any | reclass **all marked** shapes |
| `Del` / `Backspace` | delete the selected shape | delete **all marked** shapes (confirmed) |
| `H` | hide/show the selected shape(s); no-op with nothing selected | same |
| `Shift`+`H` | toggle the reveal-all overlay — draws every hidden shape without touching the hidden state; a plain `H` while it is on just turns it off | same |
| `Esc` | cancel the gesture in flight | clear the marked set |
| hold `S` | SAM3 assist mode while held | — |
| `Enter` | apply the exemplar preview (panel open) | — |
@@ -1213,7 +1216,9 @@ network.
hide; click still selects it and the trash still deletes it. Empty means the frame has
**zero shapes**, not zero visible ones: *"None on this frame — drag to draw a shape."*
plus, if the batch has shapes elsewhere, a *Jump to Frame with Shapes [N]* button.
- **Shortcuts** — a static `<dl>` of the keyboard map (it also lists `H`). Keep it visible;
While the reveal-all overlay is on (REQ-191), the header carries a small `revealing hidden`
chip — accent-coloured pill, SVG eye icon, `title` explaining `Shift+H` turns it off.
- **Shortcuts** — a static `<dl>` of the keyboard map (it also lists `H` and `Shift`+`H`). Keep it visible;
this is a keyboard-first screen and the panel is the discovery mechanism.
---
@@ -1554,7 +1559,8 @@ di-deep-link — jangan dihapus.
### Shell
Top bar tetap 48 px: wordmark kiri → nav urut pipeline di tengah → kanan badge health
(`GPU: …`, `VRAM: … GB`, `SAM3: Ready/Off` hijau saat siap, polling 3 detik) + toggle tema.
(`GPU: …`, `VRAM: … GB`, `SAM3: Loaded/Idle/Off` — `Loaded` hijau dan bisa diklik untuk
melepas VRAM (REQ-192), polling 3 detik) + toggle tema.
Error boundary membungkus area halaman, di-key per rute.
### Design token sekarang (boleh diganti, maknanya harus bertahan)