Snap2Sim / docs /features /confidence-threshold.md
jasondo
Finalize docs and archive prompts
7006f4b
|
Raw
History Blame Contribute Delete
13 kB
# Confidence Threshold: Apply Only at Analysis and Generation
Status: **Implemented and synced. Final public submission is under
`build-small-hackathon/Snap2Sim` as of June 15, 2026.**
Supersedes the prior implemented version of this file (commit `a0540e9`,
"Add confidence threshold control").
Author of spec: codebase review pass, June 14, 2026.
> **Why this supersedes the prior spec.** The slider already exists, but it was
> built as a *client-authoritative, live re-render* control: moving it instantly
> recomputes the render mode from cached analysis in the browser, and the server
> never sees the chosen threshold. The user has reversed all three of those
> decisions. The threshold must now be **applied only when analysis and
> generation run**, enforced **server-side**, and the slider must be usable
> **before the first upload**.
## Findings β€” prior behavior before this re-spec
A confidence-threshold slider already existed and worked, but not the way the
user wanted in this re-spec.
1. **Slider UI exists.** `index.html:593-597` β€” `#confidenceThreshold`
(`type="range"`, `min=0 max=1 step=0.05 value=0.5`), with a live `#thresholdValue`
label. It carries the `disabled` attribute and is only enabled after a run.
2. **Slider applies live, client-side, from cache.** The `input` handler at
`index.html:722-725` calls `updateConfidenceThreshold()` then
`scheduleThresholdRender()` (`index.html:1279-1286`), which debounces 90 ms and
re-runs `renderScenePayload(window.lastScenePayload)` against the **cached**
analysis. No network call β€” but it re-renders on *every drag*, decoupled from
any analysis/generation step.
3. **Threshold is applied client-side only.** `chooseRenderMode()`
(`index.html:821-833`) compares `analysis.confidence` against the JS
`confidenceThreshold` var. It **ignores** the `render_mode` the server already
returned in the payload.
4. **The server never receives the threshold.** `/generate_scene`
(`app.py:101-108` β†’ `_generate_scene` at `app.py:116-117` β†’
`InferenceClient.generate_scene` at `backend.py:47-51`) calls
`select_render_mode(valid_analysis)` (`schema.py:219-236`), which always uses
the hardcoded `DEFAULT_CONFIDENCE_THRESHOLD = 0.5` (`schema.py:216`). The
server's `render_mode` is computed with `0.5` regardless of the slider.
5. **Slider availability is gated on a completed run.** It is `disabled` in markup
(`index.html:595`), disabled in `resetScene()` (`index.html:769`), and
`setBusy()` (`index.html:1263`) keeps it disabled whenever
`!window.lastScenePayload`. So it cannot be set before the first analysis.
**Conclusion:** the threshold is currently a *live, browser-only* control that
never reaches the analysis/generation pipeline β€” the opposite of "only used upon
analysis and generation." This spec changes it to a value that is captured and
enforced **at generation time, server-side**, and that only takes effect on the
next run.
## Product decisions (confirmed with user, June 14, 2026)
1. **Apply timing: next run only.** Moving the slider does **nothing
immediately** β€” no live re-render of cached analysis. The new threshold is
captured and applied only the next time analysis + generation runs (next
upload / re-run). Remove the live client-side re-render entirely.
2. **Enforcement: server pipeline.** The slider value is sent to
`/generate_scene`, and the server's `select_render_mode()` uses the user's
threshold instead of the hardcoded `0.5`. The server's returned `render_mode`
becomes the source of truth; the client trusts it.
3. **Pre-run state: enabled up front.** The slider is usable before the first
upload so the chosen threshold is in effect for the very first
analysis/generation.
Unchanged from prior spec (still true):
- **Downgrade chain stays:** below threshold, skip the 3D Three.js render and
fall back to `annotate` (annotated source photo), then `unavailable`. The
slider only moves the cutoff; no new hard-block state.
- **No re-running model inference on slider move** (analysis is the expensive
Modal GPU call; it is never re-triggered by the slider).
## Implementation plan (for Codex)
Changes span the browser (`index.html`) and the server
(`app.py`, `snap2sim/backend.py`, `snap2sim/schema.py`).
### 1. Server: accept and enforce a threshold at generation
- `snap2sim/schema.py` β€” `select_render_mode(analysis, threshold=DEFAULT_CONFIDENCE_THRESHOLD)`:
- Add a `threshold` parameter, defaulting to `DEFAULT_CONFIDENCE_THRESHOLD`.
- Coerce/clamp: if `threshold` is not a number, fall back to the default; clamp
into `[0.0, 1.0]`.
- Replace the hardcoded `DEFAULT_CONFIDENCE_THRESHOLD` in the `low_confidence`
line (`schema.py:229`) with the (clamped) `threshold`.
- `snap2sim/backend.py` β€” `InferenceClient.generate_scene(analysis, threshold=None)`
(`backend.py:47`):
- Accept an optional `threshold`; when `None`/invalid use
`DEFAULT_CONFIDENCE_THRESHOLD`.
- Pass it through to `select_render_mode(valid_analysis, threshold)`.
- `app.py`:
- `/generate_scene` HTTP route (`app.py:106-108`): read
`payload.get("confidence_threshold")` and pass to `_generate_scene`.
- `@app.api(name="generate_scene")` (`app.py:101-103`): add an optional
`confidence_threshold` parameter (default keeps the existing `/run_pipeline`
Gradio API backward compatible).
- `_generate_scene(analysis, threshold)` (`app.py:116-117`): forward the
threshold to `InferenceClient(...).generate_scene(analysis, threshold)`.
- Validate at the boundary: coerce to `float`, clamp `[0, 1]`, default on
missing/invalid. Do not raise on a bad threshold β€” fall back to the default so
a malformed client value can't break generation.
Result: `/generate_scene` returns a `render_mode` computed with the user's
threshold. Existing callers that omit `confidence_threshold` still get the `0.5`
default β€” backward compatible.
### 2. Client: send the threshold at generation, stop live re-render
- **Capture and send at run time.** In `runPipeline()` (`index.html:727-759`),
include the current threshold in the generate call:
`postJson("/generate_scene", { analysis, confidence_threshold: confidenceThreshold })`
(`index.html:749`). The value is read at the moment of the call, so later slider
drags don't affect the in-flight run.
- **Trust the server's render mode.** Change `renderScenePayload()`
(`index.html:805-819`) / `chooseRenderMode()` (`index.html:821-833`) so the
decision uses `payload.render_mode` as the primary choice, with the existing
capability guards (`hasUsableGeometry`, `hasAnnotations`) only to *downgrade*
when data is missing β€” never to upgrade past what the server allowed. Remove the
client-side `confidence vs confidenceThreshold` comparison (the server now owns
that). `chooseRenderMode` should take the payload (or render_mode) rather than
recomputing from confidence.
- **Remove the live re-render.** Delete `scheduleThresholdRender()`
(`index.html:1279-1286`) and the `thresholdRenderTimer` state
(`index.html:675`). The slider `input` handler (`index.html:722-725`) should
now only call `updateConfidenceThreshold()` β€” update the `confidenceThreshold`
var and the `#thresholdValue` label. No render, no network call.
### 3. Client: enable the slider up front
- Remove the `disabled` attribute from the markup (`index.html:595`).
- In `resetScene()` (`index.html:761-776`), stop disabling the slider
(`index.html:769`) β€” it should remain available between runs.
- In `setBusy()` (`index.html:1259-1264`), disable the slider **only while a
request is in flight** (`active`), not based on `window.lastScenePayload`
(`index.html:1263`). This lets the user set the threshold before the first
upload and adjust it between runs, while preventing edits mid-request.
- Keep the default at `0.5` / `50%` so first-run behavior is unchanged when the
user never touches the slider.
### 4. Optional: reflect "applies on next run" in the UI
Because the slider no longer re-renders live, consider a subtle affordance so the
change isn't silent β€” e.g. update the label to hint the value applies to the next
analysis (tooltip or helper text). Low priority; keep it lightweight and
accessible (don't regress the existing `aria-live` label).
## Out of scope / explicitly NOT doing
- **No live re-render from cached analysis** (decision #1 β€” this is the behavior
being removed).
- **No re-running model inference (`/analyze_image`) when the slider moves**
(analysis is the expensive Modal GPU call).
- **No hard-block "confidence too low" state** β€” the `three -> annotate ->
unavailable` downgrade chain stays.
- **No model-authored HTML/JS/markup injection.** Rendering stays deterministic
Three.js from validated JSON (`SECURITY.md` Agent Guidance). The threshold is a
non-credential UX/quality control; sending it to a same-origin endpoint is fine.
## Verification checklist
- **Server, default:** `/generate_scene` without `confidence_threshold` returns
the same `render_mode` as today (regression β€” `0.5` default). Add/extend a unit
check: `select_render_mode(analysis, 0.9)` downgrades a `0.7`-confidence
geometry payload to `annotate`/`unavailable`; `select_render_mode(analysis, 0.1)`
keeps it `three`.
- **Server, clamp/coerce:** out-of-range (`-1`, `5`) and non-numeric thresholds
fall back/clamp without raising; `/generate_scene` still returns a valid payload.
- **Client, next-run-only:** moving the slider after a run does **not** trigger
any network request and does **not** change the current render (confirm via
devtools Network + visual). The new value only takes effect after the next
upload / re-run.
- **Client, enforced server-side:** raising the threshold above the analysis's
confidence and re-running downgrades the 3D cutaway to annotated photo / then
unavailable; lowering it and re-running promotes back to 3D when geometry exists.
- **Client, pre-run:** the slider is interactive before the first upload, disabled
only while a request is in flight, and re-enabled afterward.
- **Accessibility/layout (regression):** slider keyboard-operable, `#thresholdValue`
announced, does not block canvas OrbitControls (the pointer-events bug fixed in
the `docs/reviews/interaction-and-fallback-review.md` pass), and no mobile
horizontal overflow.
- `INFERENCE_BACKEND=local` sample mode still renders the example analysis with
the slider present and the threshold honored at generation.
- Existing local checks pass: schema/parser checks and FastAPI `TestClient` for
`/`, `/analyze_image`, `/generate_scene`.
## Touch points (file/line reference)
- `snap2sim/schema.py:219-236` `select_render_mode()` β€” add `threshold` param,
clamp, use it instead of the hardcoded default at `schema.py:229`.
- `snap2sim/backend.py:47-51` `generate_scene()` β€” accept + forward `threshold`.
- `app.py:101-108` `generate_scene_api` / `generate_scene_http` β€” accept
`confidence_threshold`; `app.py:116-117` `_generate_scene` β€” forward it; clamp
at the boundary.
- `index.html:595` slider markup β€” remove `disabled`.
- `index.html:722-725` slider `input` handler β€” drop the live re-render call.
- `index.html:749` `/generate_scene` call β€” send `confidence_threshold`.
- `index.html:761-776` `resetScene()` / `index.html:1259-1264` `setBusy()` β€”
enable slider up front, disable only while busy.
- `index.html:805-833` `renderScenePayload()` / `chooseRenderMode()` β€” trust
server `render_mode`; drop client-side threshold comparison.
- `index.html:675` `thresholdRenderTimer` + `index.html:1279-1286`
`scheduleThresholdRender()` β€” remove.
## Implementation result
- Implemented in commit `a6f63e9` (`Enforce confidence threshold during generation`).
- The slider is enabled before the first upload and disabled only while a request
is in flight.
- The browser sends `confidence_threshold` only with `/generate_scene`; slider
movement does not call `/analyze_image`, call `/generate_scene`, or re-render
cached analysis.
- `/generate_scene` clamps/coerces the threshold server-side and returns the
authoritative `render_mode`; the browser only downgrades when geometry or
annotation data is missing.
- Local verification passed for default/high/low/malformed/clamped thresholds,
FastAPI `TestClient`, next-run-only browser behavior, high-threshold
downgrade, low-threshold promotion, keyboard slider operation, mobile
no-overflow layout, and canvas pointer targeting.
- GitHub Actions sync run `27515950105` deployed commit `a6f63e9` to the then-private
Hugging Face Space. The Space reported SHA
`a6f63e9a0b76315bb223a09a71f4c027a29877fb`.
- Authenticated Space verification passed: the root served the updated
shell, a synthetic image returned `optical sight` at `0.7` confidence with 3
parts, high threshold returned `photo` / `annotate`, low threshold returned
`three` / `three`, and no HTML field was present.