revert: undo chained/seed-grown track 5 frames, back to b71f1d4

Reverts 1530bbc and 9c95a4a. Track 5 Frames sends the drawn box unchanged to
all five frames - no chaining, no growth. Both were rejected: chaining let a
wrong read on one frame poison the next, and growth was an attempt to follow a
moving object that the user did not want.

REQ-189, ui-spec and tasks.md return to the b71f1d4 wording.
This commit is contained in:
asus committed 2026-10-05 12:43:23 +07:00
1 parent 9c95a4a759
commit f6b0335c93
4 files changed
+27 -83

No files matched your search

+7 -14
View File
@@ -170,20 +170,13 @@ changes.
one object and the model produces its shape.
- **REQ-189** — In the review editor, the user can push the **selected shape's** box forward
with *Track 5 Frames* (`[T]`): the next five frames each get their own REQ-043 box-assist
run, and the shape is written as `source=manual`. Both geometry types work — a bbox shape
supplies its own box, a polygon is reduced to its bounding box. Runs are **independent**:
every frame is seeded from the drawn shape and never from another run's result, so a wrong
read on one frame cannot carry into the ones after it. The first frame uses the drawn box
untouched; frames 2–5 use it grown about its centre by ×1.5, giving a moving object room
to be inside the seed window. A frame SAM3 refuses (nothing inside the box) is reported and
the remaining frames still run. Nothing selected, an unusable shape, or the last frame says
so instead of doing nothing. Consequence accepted: without chaining there is no size or
position readjustment between frames, so a fast-moving object can still leave the seed
window — denser extraction (higher fps) or a video-propagation requirement (not yet
written) is the fix. This is
one-shot hand-off across the next frames, **not** propagation of drawn exemplars (which
stays a non-goal): nothing is tracked frame-to-frame with a motion model, and the runs
share no state between invocations.
run, seeded with that box, and the shape is written as `source=manual`. Both geometry types
work — a bbox shape supplies its own box, a polygon is reduced to its bounding box. Each
frame is an **independent** run: a frame SAM3 refuses (nothing inside the box) is reported
and the rest still run; nothing selected, an unusable shape, or the last frame says so
instead of doing nothing. This is one-shot hand-off across the next frames, **not**
propagation of drawn exemplars (which stays a non-goal): nothing is carried frame to frame,
and the runs do not depend on each other.
- **REQ-044** — All annotations and review statuses are **persistent** — they survive a
server restart, unlike today's in-memory sessions.
- **REQ-045** — Review progress is visible (e.g. "120/300 reviewed"), and a batch can only
-17
View File
@@ -1405,23 +1405,6 @@ read it.
(accent border + glow) — no CSS added; **browser eyeball still owed by the user**
(375 px 2-col wrap, long class names).
## Task — Track 5 Frames follows a moving object (REQ-189) `[DONE]`
1. Root cause: `trackForward` sent the **same box** to all 5 frames, and batch 19 was
extracted at **fps 0.5** (one frame every 2 s, `batches.fps`) — a truck entering frame
outruns a static seed. First attempt chained each run's result into the next frame's seed;
that let a wrong read on one frame carry into the following ones, which the user rejected
as poisoning. Final shape: runs are **independent again**, every frame seeded from the
drawn shape, frames 2–5 grown ×1.5 about its centre (`SEED_GROWTH`, `growBox`) for motion
tolerance. Geometry handling moved to module-level `shapeBox()`.
→ verify: **[DONE]** `npm run build` green; no frame's seed derives from a previous result
(`seed` reassignment removed, grep confirms `firstBox` is the only seed source); first frame
un-grown so the stationary case keeps exact precision; growth ×1.5 total size (not 2×);
busy guard still blocks double-writes. **Moving-object behaviour needs the user's GPU
run** — and without chaining, a fast-moving truck may still leave the seed window; fix is
re-extract at 2 fps (*Trim → frames per second*) or a video-propagation requirement
(not yet written; `sam3_video_inference.py` already has `propagate_in_video`).
## Task — Track 5 Frames works at all (REQ-189) `[DONE]`
1. `ReviewPage.jsx:trackForward` read `geometry.coordinates` — a key the backend never emits
+2 -10
View File
@@ -989,21 +989,13 @@ previous geometry** and shows the error. Same optimistic-with-rollback pattern f
- *Copy Prev* copies every annotation from frame `n-1` onto this frame (one POST per shape);
disabled at index 0 or when the previous frame is empty.
- *Track 5 Frames* takes the selected shape's bounding box and runs SAM3 assist on the next 5
frames with it — the first with that box exactly, the rest with it grown (see below). Works
for both geometry types: a bbox shape supplies its own box, a polygon
frames with it. Works for both geometry types: a bbox shape supplies its own box, a polygon
is reduced to its bounding box. Nothing selected, an unusable shape, or the last frame
says so in the error slot instead of silently doing nothing; a frame SAM3 refuses (nothing
inside the box) does not stop the rest — the banner then **starts** with `Tracked N of M`
(M = frames actually ahead, ≤ 5) followed by the per-frame failures. A second press while
a run is in flight is ignored. Shapes are written `source=manual`, exactly as a hand-drawn
one.
Runs are **independent**: every frame is seeded from the drawn shape, never from another
run's result, so a wrong read on one frame cannot poison the ones after it. The first frame
uses the drawn box exactly; frames 2–5 use it grown ×1.5 about its centre so a moving object
still fits the seed window. A refused frame does not stop the rest. Because nothing carries
between frames there is no size/position readjustment — a fast-moving object can still
outrun the seed; only denser frame extraction (higher fps) or real video propagation fixes
that (REQ-189).
one (REQ-189).
**Quick reclass bar** appears whenever a shape is selected: one button per class
(`[n] name`, class-coloured) plus *Delete [Del]*.
+18 -42
View File
@@ -9,38 +9,6 @@ import ReviewSidebar from '../components/ReviewSidebar'
import ExemplarFilterPanel from '../components/ExemplarFilterPanel'
import useExemplarPool from '../hooks/useExemplarPool'
// How much bigger the seed box gets for each frame after the first, so the
// object has room to have moved. Hand-tuned, not learned (REQ-189). The box
// always derives from the drawn shape — never from a previous run's result.
const SEED_GROWTH = 1.5
// SAM3 assist wants one normalized [x0, y0, x1, y1] box (REQ-043). A bbox
// shape already is that; a polygon is reduced to its bounding box.
function shapeBox(geometry) {
const pts = geometry?.points
if (geometry?.type === 'bbox' && pts?.length === 4) return pts
if (geometry?.type === 'polygon' && pts?.length) {
let minX = 1, minY = 1, maxX = 0, maxY = 0
for (const [x, y] of pts) {
if (x < minX) minX = x
if (x > maxX) maxX = x
if (y < minY) minY = y
if (y > maxY) maxY = y
}
return [minX, minY, maxX, maxY]
}
return null
}
function growBox([x0, y0, x1, y1], k) {
const cx = (x0 + x1) / 2
const cy = (y0 + y1) / 2
const hw = ((x1 - x0) * k) / 2
const hh = ((y1 - y0) * k) / 2
const clamp = (v) => Math.min(1, Math.max(0, v))
return [clamp(cx - hw), clamp(cy - hh), clamp(cx + hw), clamp(cy + hh)]
}
export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }) {
const [batch, setBatch] = useState(null)
const [project, setProject] = useState(null)
@@ -399,8 +367,23 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
return
}
const firstBox = shapeBox(current.geometry)
if (!firstBox) {
// SAM3 assist wants one normalized [x0, y0, x1, y1] box (REQ-043). A bbox
// shape already is that; a polygon is reduced to its bounding box.
const geometry = current.geometry
let box = null
if (geometry?.type === 'bbox' && geometry.points?.length === 4) {
box = geometry.points
} else if (geometry?.type === 'polygon' && geometry.points?.length) {
let minX = 1, minY = 1, maxX = 0, maxY = 0
for (const [x, y] of geometry.points) {
if (x < minX) minX = x
if (x > maxX) maxX = x
if (y < minY) minY = y
if (y > maxY) maxY = y
}
box = [minX, minY, maxX, maxY]
}
if (!box) {
setError('That shape has no usable box — Track 5 Frames needs one.')
return
}
@@ -417,15 +400,8 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
const failures = []
for (let i = 0; i < ahead.length; i++) {
const targetFrame = ahead[i]
// Every frame is seeded from the drawn box, never from a previous
// result, so a bad read on one frame cannot carry into the next.
// Frames 2-5 get it grown about its centre — the object has had
// time to move by then.
const seedBox = i === 0 ? firstBox : growBox(firstBox, SEED_GROWTH)
try {
await api.assist(targetFrame.id, {
box: seedBox, class_id: current.class_id,
})
await api.assist(targetFrame.id, { box, class_id: current.class_id })
tracked++
patchFrameLocally(targetFrame.id, { annotation_count: (targetFrame.annotation_count ?? 0) + 1 })
} catch (exc) {