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:
1 parent
f5be7880b0
commit
fd608848bf
16 files changed
+290
-20
No files matched your search
+10
-2
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user