Spaces:
Running
Running
File size: 13,034 Bytes
5e65d06 a0540e9 7006f4b 628bf77 a0540e9 628bf77 a0540e9 628bf77 a0540e9 628bf77 a0540e9 628bf77 a0540e9 628bf77 5e65d06 a0540e9 628bf77 a0540e9 628bf77 7006f4b 628bf77 7006f4b 628bf77 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | # 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.
|