fix: exemplar draw keeps other classes' full-set preview (REQ-182 amended)

- AutoAnnotateModal: render merges last full-set detections for
  non-exemplared classes with conditioned results for exemplared ones
  (selected-not-exemplared filter); drawing an example no longer clears
  the other classes' detections
- Run Preview now always refreshes full-set first (exemplars: []) then
  re-runs exemplared classes
- docs: REQ-182 amended (merge semantics), design/ui-spec/tasks synced
This commit is contained in:
asus committed 2026-10-02 15:20:46 +07:00
1 parent 8fb65d2de9
commit 8f41c6c85a
5 files changed
+59 -38

No files matched your search

+9 -6
View File
@@ -346,12 +346,15 @@ switching the active class swaps pools instead of discarding them (REQ-172). The
per class and accumulates (REQ-182): exemplar changes share **one 250 ms debounce** that
re-runs the most recently touched class against its own pool and replaces only that class's
shapes — rapid alternation between classes re-runs only the last one — while the other
exemplared classes' results stay on screen and the canvas renders the union of every class
with a non-empty pool. Undo is the same re-run with the shortened pool, because SAM3 can
only append geometric prompts. Clearing the last example sends no request and restores the
plain full-set preview (the session's last full-set run, blank if none ran yet);
Run Preview with examples runs the exemplared classes
sequentially and merges, without examples it is the unchanged full-set request. The
exemplared classes' results stay on screen and the canvas **merges** (amended REQ-182):
exemplared classes contribute their pool-conditioned results (replacing their own full-set
boxes), every other selected class keeps the detections of the last full-set run on that
frame. Undo is the same re-run with the shortened pool, because SAM3 can
only append geometric prompts. Clearing a class's last example sends no request and returns
that class to its full-set detections (blank if no full-set run covered it); clearing all
examples is then a plain full-set view again. Run Preview always refreshes the full-set for
every selected class first (with empty exemplars), then re-runs the exemplared classes
sequentially; drawing an example never triggers a full-set re-run. The
per-class overrides table also carries a **Container** checkbox (REQ-184) that toggles
`project_classes.container` through `PATCH /api/projects/{id} { containers: … }`. The same
table carries a **Copy** button (REQ-186): one click puts every selected class's effective
+8 -6
View File
@@ -129,12 +129,14 @@ changes.
pool on the current frame: switching the active class shows that class's pool instead of
discarding the others, and a new frame discards them all.
- **REQ-182** — The auto-annotate preview is **per class and accumulates**. While at least one
class has example boxes on the previewed frame, the preview shows only the classes that have
examples, each conditioned on its own pool; re-drawing one class's example refreshes only
that class's predictions and leaves the other exemplared classes' results on screen. Classes
without examples contribute nothing while any example exists, and clearing the last example
restores the plain full-set preview. The preview and the batch job still use the same
thresholds and per-class overrides.
class has example boxes on the previewed frame, the preview **merges**: classes with examples
show only their pool-conditioned results, replacing their own full-set boxes, while classes
without examples keep the detections of the last full-set run on that frame. Re-drawing one
class's example refreshes only that class's predictions and leaves everything else on screen.
Run Preview always refreshes the full-set for every selected class first, then the exemplared
ones; drawing an example never triggers a full-set re-run. Clearing a class's last example
returns that class to its full-set detections, so clearing them all is a plain full-set view
again. The preview and the batch job still use the same thresholds and per-class overrides.
- **REQ-186** — The auto-annotate parameters table has a **copy button** that puts every
selected class's effective settings on the clipboard as short text, one class per line:
`class <name> conf <v> iou <v> minbox <v> container <true|false>`. Effective means the
+17 -4
View File
@@ -1272,10 +1272,11 @@ failure modes to the counting path. The saving was always on the browser side.
1. Exemplar pools keyed by class and a per-class accumulating preview in the auto-annotate
modal — switching the active class swaps pools and a frame change clears them all
(REQ-172); a redraw re-runs only the touched class against its own pool, the canvas unions
every exemplared class, classes without examples contribute nothing while any example
exists, clearing the last example falls back to the full-set preview, no examples at all →
the unchanged full-set request (REQ-182) → verify: **[DONE]** `cd frontend && npm run
(REQ-172); a redraw re-runs only the touched class against its own pool, the canvas merges
exemplared classes' conditioned results over the last full-set detections of the other
selected classes (REQ-182, as amended — superseded by the merged-preview entry below),
clearing the last example returns that class to its full-set detections, no examples at
all → the unchanged full-set request (REQ-182) → verify: **[DONE]** `cd frontend && npm run
build` passes (✓ 536 ms, 69 modules); `wc -l` caps hold —
`frontend/src/components/AutoAnnotateModal.jsx` (400) and
`frontend/src/hooks/useExemplarPools.js` (100) both ≤ 400; rebuilt Docker frontend
@@ -1348,6 +1349,18 @@ read it.
invalid→global, float-noise case 0.30000000000000004 → `0.3`); browser clipboard
click-test NOT automated — manual click-test pending.
## Task — Merged preview: exemplar draw keeps other classes (REQ-182 amended) `[DONE]`
1. Non-exemplared classes keep last full-set detections when an exemplar is drawn;
exemplared class shows conditioned results replacing its own; Run Preview
refreshes full-set first (exemplars: []) then exemplared; draw never full-set
re-runs → verify: **[DONE]** `cd frontend && npm run build` passes (✓ 523 ms);
`wc -l frontend/src/components/AutoAnnotateModal.jsx` = 400 (at cap; blank lines
trimmed to fit, adjudicated by review); `git diff --stat` = 1 code file; grep
proof full-set call has `exemplars: []` (`AutoAnnotateModal.jsx:118`); 7-case
walkthrough in t11-report.md; browser test (draw example after Run Preview, other
classes persist) NOT automated — manual click-test pending.
## Known open points
- *Not closed by any task, by choice:* **any rebuild kills the running job.** Task 14's resume
+8 -5
View File
@@ -485,11 +485,14 @@ them all. Adding, undoing or clearing an exemplar records the touched class and
single revision counter — **one shared debounce** across all classes, 250 ms — which re-runs
the most recently touched class against its own pool and replaces only its shapes, so rapid
alternation between classes re-runs only the last one; the other exemplared classes' results
stay on screen and the canvas shows the union of every class with examples. Classes without
examples contribute nothing while any example exists; Run Preview with examples runs those
classes sequentially and merges, and with none it is the plain full-set preview. Clearing the
last example sends no request and restores the plain full-set preview (the session's last
full-set run, blank if none ran yet). Drawing a box *is* the question and the redrawn
stay on screen and the canvas **merges** (amended REQ-182): exemplared classes contribute
their pool-conditioned results (replacing their own full-set boxes), every other selected
class keeps the detections of the last full-set run on that frame. Classes without examples
keep those detections on screen; Run Preview always refreshes the full-set first (empty
exemplars), then re-runs the exemplared classes sequentially — drawing an example never
triggers a full-set re-run. Clearing a class's last example sends no request and returns
that class to its full-set detections (blank if no full-set run covered it); clearing them
all is a plain full-set view again. Drawing a box *is* the question and the redrawn
preview is the answer, so no button press sits between them. Re-running is cheap because
`set_image` is already cached for this frame — only the grounding head runs.
+17 -17
View File
@@ -20,7 +20,6 @@ export default function AutoAnnotateModal({
const [minBoxFrac, setMinBoxFrac] = useState(0.0)
const [classParams, setClassParams] = useState({})
// Class selection state
const [selectedClasses, setSelectedClasses] = useState(() => {
if (engine === 'base_model') {
@@ -85,20 +84,25 @@ export default function AutoAnnotateModal({
})
const activePool = activeClassName ? pools[activeClassName] || [] : []
// While any selected class has examples, only those classes contribute.
const exemplaredClasses = selectedClasses.filter(name => pools[name]?.length)
// merged: exemplared classes show conditioned results (replacing their full-set
// boxes); everyone else keeps the last full-set run (amended REQ-182)
const nameById = Object.fromEntries(project.classes.map(c => [c.class_id, c.name]))
const exemplaredSet = new Set(exemplaredClasses)
const selectedSet = new Set(selectedClasses)
const renderShapes = exemplaredClasses.length > 0
? exemplaredClasses.flatMap(name => shapesByClass[name] || [])
? [
...previewShapes.filter(s => {
const n = nameById[s.class_id]
return selectedSet.has(n) && !exemplaredSet.has(n)
}),
...exemplaredClasses.flatMap(name => shapesByClass[name] || []),
]
: previewShapes
const handlePreview = () => {
if (exemplaredClasses.length > 0) {
run(exemplaredClasses)
return
}
const frame = currentFrame
if (!frame) return
let isMounted = true
setIsLoadingPreview(true)
setPreviewError('')
@@ -111,8 +115,8 @@ export default function AutoAnnotateModal({
target_class_names: selectedClasses,
class_params: buildClassParams(classParams),
custom_model_path: customModelStagedPath,
exemplars: engine === 'sam3' ? activePool : [],
exemplar_class_name: engine === 'sam3' ? activeClassName : null
exemplars: [],
exemplar_class_name: null
}).then(res => {
if (isMounted && res.shapes) {
setPreviewShapes(res.shapes)
@@ -123,7 +127,9 @@ export default function AutoAnnotateModal({
setPreviewError(err.message || 'Preview failed')
setPreviewShapes([])
}).finally(() => {
setIsLoadingPreview(false)
if (!isMounted) return
if (exemplaredClasses.length > 0) run(exemplaredClasses) // manages busy itself
else setIsLoadingPreview(false)
})
}
@@ -212,11 +218,9 @@ export default function AutoAnnotateModal({
border: '1px solid rgba(168, 85, 247, 0.4)', background: '#18181b',
overflowY: 'auto', borderRadius: 8
}}>
{/* Left Side: Preview */}
<div style={{ flex: '1 1 450px', display: 'flex', flexDirection: 'column', minWidth: 280 }}>
<h3 style={{ margin: '0 0 8px 0', color: '#e4e4e7', fontSize: '1rem' }}>Preview</h3>
<div style={{ position: 'relative', width: '100%', background: '#000', borderRadius: 6, overflow: 'hidden', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{currentFrame ? (
/* This wrapper must hug the image and nothing else: both overlays
@@ -294,7 +298,6 @@ export default function AutoAnnotateModal({
</>
)}
</div>
<div style={{ marginTop: 10 }}>
<div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}>
<span className="hint" style={{ fontSize: '0.8rem' }}>Preview Frame ({frameIndex + 1} / {frames.length}):</span>
@@ -380,7 +383,6 @@ export default function AutoAnnotateModal({
promptError={promptError}
savedPrompt={activeClass ? classPrompts[activeClass.class_id] : ''}
/>
<div className="row" style={{ justifyContent: 'flex-end', gap: 10, marginTop: 16, paddingTop: 10, borderTop: '1px solid rgba(255,255,255,0.08)' }}>
<button className="btn btn-ghost" onClick={onClose} disabled={isSubmitting}>Cancel</button>
<button
@@ -392,9 +394,7 @@ export default function AutoAnnotateModal({
</button>
</div>
</div>
</div>
</div>
)
}