Snap2Sim / docs /features /confidence-threshold.md
jasondo
Finalize docs and archive prompts
7006f4b
|
Raw
History Blame Contribute Delete
13 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade

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.