fix: track 5 frames works for bbox and polygon shapes (REQ-189)

trackForward read geometry.coordinates, a key the backend never emits, and
had no bbox branch - the action silently no-op'd for every shape (flagged
dead in docs/audit-2026-08-07.md:103).

- bbox points used as-is, polygon reduced to its bounding box
- guards speak instead of returning silently
- a frame SAM3 refuses no longer aborts the remaining frames
- a press during an in-flight run is ignored (would double-write)
- REQ-189 added; non-goal requirements.md:22 amended to keep exemplar
  propagation out while admitting this one-shot hand-off
This commit is contained in:
asus committed 2026-10-05 12:01:59 +07:00
1 parent 2971f9192c
commit b71f1d4bf2
4 files changed
+79 -24

No files matched your search

+42 -19
View File
@@ -355,15 +355,27 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
}, [frames, index, frame, hiddenClasses])
const trackForward = useCallback(async () => {
if (selectedId == null || !frames?.length) return
if (busy) return
if (!frames?.length) return
if (selectedId == null) {
setError('Select a shape first — Track 5 Frames follows one shape forward.')
return
}
const current = annotations.find((a) => a.id === selectedId)
if (!current) return
if (!current) {
setError('That shape is gone — select another one.')
return
}
// 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 (current.geometry?.type === 'polygon' && current.geometry.coordinates?.[0]) {
const pts = current.geometry.coordinates[0]
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 pts) {
for (const [x, y] of geometry.points) {
if (x < minX) minX = x
if (x > maxX) maxX = x
if (y < minY) minY = y
@@ -371,27 +383,38 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
}
box = [minX, minY, maxX, maxY]
}
if (!box) return
if (!box) {
setError('That shape has no usable box — Track 5 Frames needs one.')
return
}
const ahead = frames.slice(index + 1, index + 6)
if (!ahead.length) {
setError('Last frame — Track 5 Frames has no frames ahead to follow into.')
return
}
setBusy(true)
setError('')
try {
let updatedCount = 0
for (let i = 1; i <= 5; i++) {
const nextIdx = index + i
if (nextIdx >= frames.length) break
const targetFrame = frames[nextIdx]
await api.assist(targetFrame.id, { box, class_id: current.class_id })
updatedCount++
patchFrameLocally(targetFrame.id, { annotation_count: (targetFrame.annotation_count ?? 0) + 1 })
let tracked = 0
const failures = []
for (let i = 0; i < ahead.length; i++) {
const targetFrame = ahead[i]
try {
await api.assist(targetFrame.id, { box, class_id: current.class_id })
tracked++
patchFrameLocally(targetFrame.id, { annotation_count: (targetFrame.annotation_count ?? 0) + 1 })
} catch (exc) {
failures.push(`frame ${index + i + 2}: ${exc.message}`)
}
}
if (failures.length) {
setError(`Tracked ${tracked} of ${ahead.length} — ${failures.join(' · ')}`)
}
} catch (exc) {
setError(exc.message)
} finally {
setBusy(false)
}
}, [annotations, selectedId, index, frames])
}, [annotations, selectedId, index, frames, busy])
const stateRef = useRef({})
stateRef.current = { frames, index, project, selectedId, setStatus, removeSelected, reclass, jumpToPending, jumpToNextAnnotated, setAssistMode, copyPrevious, trackForward, mode, setMode, markedIds, removeMarked, reclassMarked, setMarkedIds, toggleHideSelected }