Spaces:
Running
Running
| # 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. | |