jasondo OpenAI Codex commited on
Commit
628bf77
Β·
1 Parent(s): a6f63e9

Document server-side confidence threshold

Browse files

Co-authored-by: OpenAI Codex <codex@openai.com>

Files changed (2) hide show
  1. FEATURE.md +208 -181
  2. README.md +8 -6
FEATURE.md CHANGED
@@ -1,200 +1,227 @@
1
- # FEATURE: Confidence Threshold Control for Cutaway Generation
2
 
3
- Status: Implemented and synced to the private Space, June 14, 2026.
 
 
4
  Author of spec: codebase review pass, June 14, 2026.
5
 
6
- ## Summary
7
-
8
- Add a user-facing **confidence threshold slider** that controls how high the
9
- model's analysis `confidence` must be before the deterministic Three.js cutaway
10
- is rendered. Today this threshold exists but is a hardcoded `0.5` magic number,
11
- duplicated across Python and JavaScript, and not user-controllable.
12
-
13
- ## Findings (current behavior)
14
-
15
- **Q: Does the generation of the Three.js parts have a confidence threshold to
16
- pass before sending back to the user?**
17
-
18
- Partially yes, but it is implicit, hardcoded, and not a true block:
19
-
20
- 1. The vision model returns a `confidence` value (`0`–`1`) in the analysis JSON.
21
- See `snap2sim/schema.py` (`ANALYSIS_SCHEMA`, `confidence` is
22
- `number, minimum 0, maximum 1`), `snap2sim/prompts.py`, and
23
- `snap2sim/model_io.py` (defaults: coerced `0.55`, generic fallback `0.45`).
24
-
25
- 2. Confidence gates the **render mode**, not whether output is returned. The
26
- threshold is a hardcoded `0.5` and lives in **two** places:
27
- - **Python** `snap2sim/schema.py:218` `select_render_mode()`:
28
- `low_confidence = _is_number(confidence) and float(confidence) < 0.5`.
29
- This is called by `snap2sim/backend.py:47` `InferenceClient.generate_scene()`,
30
- which returns `render_mode` in the `/generate_scene` payload.
31
- - **JavaScript** `index.html:760` `chooseRenderMode()`:
32
- `confidence < 0.5` / `confidence >= 0.5`. Used as a client-side fallback
33
- only when the server payload omits `render_mode`.
34
-
35
- 3. The threshold **downgrades** rather than blocks. The chain is:
36
- `three` (3D cutaway) -> `annotate` (annotated source photo overlay) ->
37
- `unavailable` (honest "cannot render" state). A low-confidence analysis is
38
- still returned to the user, just not as 3D parts. See
39
- `index.html:748` `renderScenePayload()` and `index.html:760`
40
- `chooseRenderMode()`; mirrored in `schema.py:218` `select_render_mode()`.
41
-
42
- 4. Confidence is currently only **displayed** (as a percentage) at
43
- `index.html:726`; there is no control to change the cutoff.
44
-
45
- 5. There is **no server-side hard gate**. `modal_app.py` / `app.py` validate and
46
- coerce the payload but never reject on low confidence.
47
-
48
- **Conclusion:** an implicit confidence threshold (`0.5`) already governs whether
49
- Three.js parts render, but it is a duplicated magic number with no UI control.
50
- This feature exposes it as a slider and removes the magic-number duplication.
 
51
 
52
  ## Product decisions (confirmed with user, June 14, 2026)
53
 
54
- 1. **Gate behavior: keep the downgrade chain.** Below threshold, skip the 3D
55
- Three.js render and fall back to annotated photo, then unavailable. The
56
- slider only moves the cutoff; it does not introduce a new hard-block state.
57
- 2. **Slider apply: live re-render from cached analysis.** Moving the slider after
58
- an analysis returns must re-evaluate the render mode against the
59
- already-returned analysis JSON and re-render immediately. No new
60
- `/analyze_image` or model call β€” zero added latency or Modal cost.
61
- 3. **Source of truth: client authoritative.** The browser owns the threshold.
62
- The client recomputes the render mode from the raw `confidence` plus the
63
- slider value, overriding any `render_mode` in the server payload. Rendering is
64
- already deterministic browser-side, so this is the minimal change. Per
65
- `SECURITY.md`, the threshold is a non-credential UX control, so client
66
- authority is acceptable; it is a quality gate, not a security gate.
 
 
 
 
 
67
 
68
  ## Implementation plan (for Codex)
69
 
70
- All primary changes are in `index.html`. No backend signature change is required.
71
-
72
- ### 1. Add the slider UI
73
-
74
- - Add a labeled `range` input to the viewport toolbar
75
- (`index.html:550` `<div class="toolbar">`) or the readout panel near the
76
- confidence metric (`index.html:577` `metric-row`). Recommended: toolbar, next
77
- to `resetViewButton`, so it sits with the other live viewport controls.
78
- - Suggested markup:
79
- ```html
80
- <label class="threshold-control" for="confidenceThreshold">
81
- Min confidence
82
- <input id="confidenceThreshold" class="threshold-slider" type="range"
83
- min="0" max="1" step="0.05" value="0.5"
84
- aria-describedby="thresholdValue">
85
- <span id="thresholdValue" aria-live="polite">50%</span>
86
- </label>
87
- ```
88
- - Default value `0.5` to preserve current behavior exactly.
89
- - Style consistent with existing `.tool-button` / toolbar aesthetic; keep it
90
- keyboard-accessible (range inputs are by default) and screen-reader labeled,
91
- matching the accessibility work already done in this repo (live status,
92
- keyboard drop zone).
93
-
94
- ### 2. Wire the threshold into render-mode selection
95
-
96
- - Introduce a single source for the current threshold, e.g.
97
- `let confidenceThreshold = 0.5;`, updated from the slider's `input` event.
98
- - Update the slider value label (`#thresholdValue`) on `input`.
99
- - Replace the hardcoded `0.5` comparisons in `chooseRenderMode()`
100
- (`index.html:760-772`) with `confidenceThreshold`:
101
- - `confidence < confidenceThreshold` for the low-confidence branch.
102
- - `confidence >= confidenceThreshold` for the `three` branch.
103
- - Make `renderScenePayload()` (`index.html:748`) **client authoritative**:
104
- always compute the render mode via `chooseRenderMode(analysis)` using the
105
- current threshold, instead of trusting `payload.render_mode`. Keep the
106
- geometry/annotation capability checks (`hasUsableGeometry`, `hasAnnotations`)
107
- so a high threshold never forces a 3D render that lacks geometry.
108
-
109
- ### 3. Live re-render on slider change
110
-
111
- - Cache the last analysis (already stored at `index.html:722`
112
- `window.lastAnalysis`) and the last scene payload.
113
- - On slider `change` (or debounced `input`), if an analysis is present and the
114
- pipeline is not mid-request, call `renderScenePayload(lastScenePayload)` (or a
115
- small `reevaluateRender()` helper that reads `window.lastAnalysis`) to rebuild
116
- the scene from cached data. Do **not** call `/analyze_image` or
117
- `/generate_scene` again β€” re-use the cached analysis JSON.
118
- - Guard against re-render while `setBusy(true)` is active to avoid racing an
119
- in-flight request.
120
- - Reset/standby state should disable or ignore the slider re-render until an
121
- analysis exists.
122
-
123
- ### 4. Remove the magic-number duplication (recommended cleanup)
124
-
125
- Because decision #3 makes the client authoritative, the server's
126
- `select_render_mode()` `0.5` is now advisory only. Two acceptable options:
127
-
128
- - **Minimal:** leave `schema.py` `select_render_mode()` as-is (still returns a
129
- reasonable default `render_mode`); the client overrides it. Add a code comment
130
- noting the client is authoritative for the user-facing threshold.
131
- - **Cleaner (preferred if time allows):** extract the `0.5` default into a single
132
- named constant, e.g. `DEFAULT_CONFIDENCE_THRESHOLD = 0.5` in `schema.py`, and a
133
- matching JS constant in `index.html`, so the default lives in one obvious place
134
- per layer. Do **not** add a backend signature change to pass the slider value
135
- to the server (decision #3 keeps the threshold client-side).
 
 
 
 
 
136
 
137
  ## Out of scope / explicitly NOT doing
138
 
139
- - No new server endpoint or `/generate_scene` signature change (client
140
- authoritative per decision #3).
141
- - No hard-block "confidence too low" state β€” the downgrade chain stays
142
- (decision #1).
143
- - No re-running model inference when the slider moves (decision #2).
144
- - No model-authored HTML/JS/markup injection; rendering stays deterministic
145
- Three.js from validated JSON (`SECURITY.md` Agent Guidance).
146
-
147
- ## Implementation result
148
-
149
- - Added a toolbar confidence threshold slider in `index.html`, defaulting to
150
- `0.5` / `50%`.
151
- - The browser now recomputes render mode from the cached analysis confidence and
152
- current slider value, overriding advisory server `render_mode`.
153
- - Slider input re-renders from `window.lastScenePayload` only; it does not call
154
- `/analyze_image` or `/generate_scene`.
155
- - Added `DEFAULT_CONFIDENCE_THRESHOLD = 0.5` in `snap2sim/schema.py` for the
156
- server's advisory fallback.
157
- - Local verification passed: schema/parser check, FastAPI `TestClient` root /
158
- `/analyze_image` / `/generate_scene`, browser slider downgrade/promote with
159
- no network calls, annotated-photo fallback, keyboard slider operation, mobile
160
- no horizontal overflow, and canvas pointer targeting after normal overlay
161
- hiding.
162
- - GitHub Actions sync run `27515143658` deployed commit `a0540e9` to the
163
- private Hugging Face Space. The Space reported the same SHA and remained
164
- private. Authenticated deployed verification confirmed the updated root shell
165
- contains the slider, a synthetic target image returned `Target Reticle` at
166
- `0.7` confidence with 3 parts, and `/generate_scene` returned
167
- `renderer: three`, `render_mode: three`, and no HTML field.
168
 
169
  ## Verification checklist
170
 
171
- - Slider defaults to `0.5`; with default value, behavior matches current
172
- production exactly (regression check).
173
- - Raising the threshold above a returned analysis's confidence downgrades a
174
- previously-3D render to annotated photo, then to unavailable, live, with no
175
- network call (confirm via browser devtools Network tab β€” no new
176
- `/analyze_image` or `/generate_scene` requests on slider move).
177
- - Lowering the threshold promotes an annotated/unavailable result back to the 3D
178
- cutaway, provided usable geometry exists.
179
- - Slider value label updates and is announced (`aria-live`).
180
- - Slider is keyboard-operable and does not steal focus from / block the canvas
181
- OrbitControls (watch for the pointer-events class of bug fixed in the
182
- REVIEW2.md pass).
183
- - Mobile layout: slider does not introduce horizontal overflow; toolbar still
184
- fits.
 
 
 
 
 
185
  - `INFERENCE_BACKEND=local` sample mode still renders the example analysis with
186
- the slider present.
187
- - Run existing local checks: schema/parser checks and the FastAPI `TestClient`
188
- pass for `/`, `/analyze_image`, `/generate_scene`.
189
 
190
  ## Touch points (file/line reference)
191
 
192
- - `index.html:550` toolbar β€” add slider markup.
193
- - `index.html:577` metric-row / `index.html:726` confidence display β€” optional
194
- co-location with confidence readout.
195
- - `index.html:748` `renderScenePayload()` β€” make client authoritative.
196
- - `index.html:760` `chooseRenderMode()` β€” replace `0.5` with slider value.
197
- - `index.html:602-622` element refs + `index.html:624` state vars β€” add slider
198
- element ref and `confidenceThreshold` state.
199
- - `snap2sim/schema.py:218` `select_render_mode()` β€” optional constant extraction.
200
- - `snap2sim/backend.py:47` `generate_scene()` β€” no change required.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FEATURE: Confidence Threshold β€” Apply Only at Analysis & Generation
2
 
3
+ Status: **Implemented and synced to the private Space, June 14, 2026.**
4
+ Supersedes the prior implemented version of this file (commit `a0540e9`,
5
+ "Add confidence threshold control").
6
  Author of spec: codebase review pass, June 14, 2026.
7
 
8
+ > **Why this supersedes the prior spec.** The slider already exists, but it was
9
+ > built as a *client-authoritative, live re-render* control: moving it instantly
10
+ > recomputes the render mode from cached analysis in the browser, and the server
11
+ > never sees the chosen threshold. The user has reversed all three of those
12
+ > decisions. The threshold must now be **applied only when analysis and
13
+ > generation run**, enforced **server-side**, and the slider must be usable
14
+ > **before the first upload**.
15
+
16
+ ## Findings β€” prior behavior before this re-spec
17
+
18
+ A confidence-threshold slider already existed and worked, but not the way the
19
+ user wanted in this re-spec.
20
+
21
+ 1. **Slider UI exists.** `index.html:593-597` β€” `#confidenceThreshold`
22
+ (`type="range"`, `min=0 max=1 step=0.05 value=0.5`), with a live `#thresholdValue`
23
+ label. It carries the `disabled` attribute and is only enabled after a run.
24
+
25
+ 2. **Slider applies live, client-side, from cache.** The `input` handler at
26
+ `index.html:722-725` calls `updateConfidenceThreshold()` then
27
+ `scheduleThresholdRender()` (`index.html:1279-1286`), which debounces 90 ms and
28
+ re-runs `renderScenePayload(window.lastScenePayload)` against the **cached**
29
+ analysis. No network call β€” but it re-renders on *every drag*, decoupled from
30
+ any analysis/generation step.
31
+
32
+ 3. **Threshold is applied client-side only.** `chooseRenderMode()`
33
+ (`index.html:821-833`) compares `analysis.confidence` against the JS
34
+ `confidenceThreshold` var. It **ignores** the `render_mode` the server already
35
+ returned in the payload.
36
+
37
+ 4. **The server never receives the threshold.** `/generate_scene`
38
+ (`app.py:101-108` β†’ `_generate_scene` at `app.py:116-117` β†’
39
+ `InferenceClient.generate_scene` at `backend.py:47-51`) calls
40
+ `select_render_mode(valid_analysis)` (`schema.py:219-236`), which always uses
41
+ the hardcoded `DEFAULT_CONFIDENCE_THRESHOLD = 0.5` (`schema.py:216`). The
42
+ server's `render_mode` is computed with `0.5` regardless of the slider.
43
+
44
+ 5. **Slider availability is gated on a completed run.** It is `disabled` in markup
45
+ (`index.html:595`), disabled in `resetScene()` (`index.html:769`), and
46
+ `setBusy()` (`index.html:1263`) keeps it disabled whenever
47
+ `!window.lastScenePayload`. So it cannot be set before the first analysis.
48
+
49
+ **Conclusion:** the threshold is currently a *live, browser-only* control that
50
+ never reaches the analysis/generation pipeline β€” the opposite of "only used upon
51
+ analysis and generation." This spec changes it to a value that is captured and
52
+ enforced **at generation time, server-side**, and that only takes effect on the
53
+ next run.
54
 
55
  ## Product decisions (confirmed with user, June 14, 2026)
56
 
57
+ 1. **Apply timing: next run only.** Moving the slider does **nothing
58
+ immediately** β€” no live re-render of cached analysis. The new threshold is
59
+ captured and applied only the next time analysis + generation runs (next
60
+ upload / re-run). Remove the live client-side re-render entirely.
61
+ 2. **Enforcement: server pipeline.** The slider value is sent to
62
+ `/generate_scene`, and the server's `select_render_mode()` uses the user's
63
+ threshold instead of the hardcoded `0.5`. The server's returned `render_mode`
64
+ becomes the source of truth; the client trusts it.
65
+ 3. **Pre-run state: enabled up front.** The slider is usable before the first
66
+ upload so the chosen threshold is in effect for the very first
67
+ analysis/generation.
68
+
69
+ Unchanged from prior spec (still true):
70
+ - **Downgrade chain stays:** below threshold, skip the 3D Three.js render and
71
+ fall back to `annotate` (annotated source photo), then `unavailable`. The
72
+ slider only moves the cutoff; no new hard-block state.
73
+ - **No re-running model inference on slider move** (analysis is the expensive
74
+ Modal GPU call; it is never re-triggered by the slider).
75
 
76
  ## Implementation plan (for Codex)
77
 
78
+ Changes span the browser (`index.html`) and the server
79
+ (`app.py`, `snap2sim/backend.py`, `snap2sim/schema.py`).
80
+
81
+ ### 1. Server: accept and enforce a threshold at generation
82
+
83
+ - `snap2sim/schema.py` β€” `select_render_mode(analysis, threshold=DEFAULT_CONFIDENCE_THRESHOLD)`:
84
+ - Add a `threshold` parameter, defaulting to `DEFAULT_CONFIDENCE_THRESHOLD`.
85
+ - Coerce/clamp: if `threshold` is not a number, fall back to the default; clamp
86
+ into `[0.0, 1.0]`.
87
+ - Replace the hardcoded `DEFAULT_CONFIDENCE_THRESHOLD` in the `low_confidence`
88
+ line (`schema.py:229`) with the (clamped) `threshold`.
89
+ - `snap2sim/backend.py` β€” `InferenceClient.generate_scene(analysis, threshold=None)`
90
+ (`backend.py:47`):
91
+ - Accept an optional `threshold`; when `None`/invalid use
92
+ `DEFAULT_CONFIDENCE_THRESHOLD`.
93
+ - Pass it through to `select_render_mode(valid_analysis, threshold)`.
94
+ - `app.py`:
95
+ - `/generate_scene` HTTP route (`app.py:106-108`): read
96
+ `payload.get("confidence_threshold")` and pass to `_generate_scene`.
97
+ - `@app.api(name="generate_scene")` (`app.py:101-103`): add an optional
98
+ `confidence_threshold` parameter (default keeps the existing `/run_pipeline`
99
+ Gradio API backward compatible).
100
+ - `_generate_scene(analysis, threshold)` (`app.py:116-117`): forward the
101
+ threshold to `InferenceClient(...).generate_scene(analysis, threshold)`.
102
+ - Validate at the boundary: coerce to `float`, clamp `[0, 1]`, default on
103
+ missing/invalid. Do not raise on a bad threshold β€” fall back to the default so
104
+ a malformed client value can't break generation.
105
+
106
+ Result: `/generate_scene` returns a `render_mode` computed with the user's
107
+ threshold. Existing callers that omit `confidence_threshold` still get the `0.5`
108
+ default β€” backward compatible.
109
+
110
+ ### 2. Client: send the threshold at generation, stop live re-render
111
+
112
+ - **Capture and send at run time.** In `runPipeline()` (`index.html:727-759`),
113
+ include the current threshold in the generate call:
114
+ `postJson("/generate_scene", { analysis, confidence_threshold: confidenceThreshold })`
115
+ (`index.html:749`). The value is read at the moment of the call, so later slider
116
+ drags don't affect the in-flight run.
117
+ - **Trust the server's render mode.** Change `renderScenePayload()`
118
+ (`index.html:805-819`) / `chooseRenderMode()` (`index.html:821-833`) so the
119
+ decision uses `payload.render_mode` as the primary choice, with the existing
120
+ capability guards (`hasUsableGeometry`, `hasAnnotations`) only to *downgrade*
121
+ when data is missing β€” never to upgrade past what the server allowed. Remove the
122
+ client-side `confidence vs confidenceThreshold` comparison (the server now owns
123
+ that). `chooseRenderMode` should take the payload (or render_mode) rather than
124
+ recomputing from confidence.
125
+ - **Remove the live re-render.** Delete `scheduleThresholdRender()`
126
+ (`index.html:1279-1286`) and the `thresholdRenderTimer` state
127
+ (`index.html:675`). The slider `input` handler (`index.html:722-725`) should
128
+ now only call `updateConfidenceThreshold()` β€” update the `confidenceThreshold`
129
+ var and the `#thresholdValue` label. No render, no network call.
130
+
131
+ ### 3. Client: enable the slider up front
132
+
133
+ - Remove the `disabled` attribute from the markup (`index.html:595`).
134
+ - In `resetScene()` (`index.html:761-776`), stop disabling the slider
135
+ (`index.html:769`) β€” it should remain available between runs.
136
+ - In `setBusy()` (`index.html:1259-1264`), disable the slider **only while a
137
+ request is in flight** (`active`), not based on `window.lastScenePayload`
138
+ (`index.html:1263`). This lets the user set the threshold before the first
139
+ upload and adjust it between runs, while preventing edits mid-request.
140
+ - Keep the default at `0.5` / `50%` so first-run behavior is unchanged when the
141
+ user never touches the slider.
142
+
143
+ ### 4. Optional: reflect "applies on next run" in the UI
144
+
145
+ Because the slider no longer re-renders live, consider a subtle affordance so the
146
+ change isn't silent β€” e.g. update the label to hint the value applies to the next
147
+ analysis (tooltip or helper text). Low priority; keep it lightweight and
148
+ accessible (don't regress the existing `aria-live` label).
149
 
150
  ## Out of scope / explicitly NOT doing
151
 
152
+ - **No live re-render from cached analysis** (decision #1 β€” this is the behavior
153
+ being removed).
154
+ - **No re-running model inference (`/analyze_image`) when the slider moves**
155
+ (analysis is the expensive Modal GPU call).
156
+ - **No hard-block "confidence too low" state** β€” the `three -> annotate ->
157
+ unavailable` downgrade chain stays.
158
+ - **No model-authored HTML/JS/markup injection.** Rendering stays deterministic
159
+ Three.js from validated JSON (`SECURITY.md` Agent Guidance). The threshold is a
160
+ non-credential UX/quality control; sending it to a same-origin endpoint is fine.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
  ## Verification checklist
163
 
164
+ - **Server, default:** `/generate_scene` without `confidence_threshold` returns
165
+ the same `render_mode` as today (regression β€” `0.5` default). Add/extend a unit
166
+ check: `select_render_mode(analysis, 0.9)` downgrades a `0.7`-confidence
167
+ geometry payload to `annotate`/`unavailable`; `select_render_mode(analysis, 0.1)`
168
+ keeps it `three`.
169
+ - **Server, clamp/coerce:** out-of-range (`-1`, `5`) and non-numeric thresholds
170
+ fall back/clamp without raising; `/generate_scene` still returns a valid payload.
171
+ - **Client, next-run-only:** moving the slider after a run does **not** trigger
172
+ any network request and does **not** change the current render (confirm via
173
+ devtools Network + visual). The new value only takes effect after the next
174
+ upload / re-run.
175
+ - **Client, enforced server-side:** raising the threshold above the analysis's
176
+ confidence and re-running downgrades the 3D cutaway to annotated photo / then
177
+ unavailable; lowering it and re-running promotes back to 3D when geometry exists.
178
+ - **Client, pre-run:** the slider is interactive before the first upload, disabled
179
+ only while a request is in flight, and re-enabled afterward.
180
+ - **Accessibility/layout (regression):** slider keyboard-operable, `#thresholdValue`
181
+ announced, does not block canvas OrbitControls (the pointer-events bug fixed in
182
+ the REVIEW2.md pass), and no mobile horizontal overflow.
183
  - `INFERENCE_BACKEND=local` sample mode still renders the example analysis with
184
+ the slider present and the threshold honored at generation.
185
+ - Existing local checks pass: schema/parser checks and FastAPI `TestClient` for
186
+ `/`, `/analyze_image`, `/generate_scene`.
187
 
188
  ## Touch points (file/line reference)
189
 
190
+ - `snap2sim/schema.py:219-236` `select_render_mode()` β€” add `threshold` param,
191
+ clamp, use it instead of the hardcoded default at `schema.py:229`.
192
+ - `snap2sim/backend.py:47-51` `generate_scene()` β€” accept + forward `threshold`.
193
+ - `app.py:101-108` `generate_scene_api` / `generate_scene_http` β€” accept
194
+ `confidence_threshold`; `app.py:116-117` `_generate_scene` β€” forward it; clamp
195
+ at the boundary.
196
+ - `index.html:595` slider markup β€” remove `disabled`.
197
+ - `index.html:722-725` slider `input` handler β€” drop the live re-render call.
198
+ - `index.html:749` `/generate_scene` call β€” send `confidence_threshold`.
199
+ - `index.html:761-776` `resetScene()` / `index.html:1259-1264` `setBusy()` β€”
200
+ enable slider up front, disable only while busy.
201
+ - `index.html:805-833` `renderScenePayload()` / `chooseRenderMode()` β€” trust
202
+ server `render_mode`; drop client-side threshold comparison.
203
+ - `index.html:675` `thresholdRenderTimer` + `index.html:1279-1286`
204
+ `scheduleThresholdRender()` β€” remove.
205
+
206
+ ## Implementation result
207
+
208
+ - Implemented in commit `a6f63e9` (`Enforce confidence threshold during generation`).
209
+ - The slider is enabled before the first upload and disabled only while a request
210
+ is in flight.
211
+ - The browser sends `confidence_threshold` only with `/generate_scene`; slider
212
+ movement does not call `/analyze_image`, call `/generate_scene`, or re-render
213
+ cached analysis.
214
+ - `/generate_scene` clamps/coerces the threshold server-side and returns the
215
+ authoritative `render_mode`; the browser only downgrades when geometry or
216
+ annotation data is missing.
217
+ - Local verification passed for default/high/low/malformed/clamped thresholds,
218
+ FastAPI `TestClient`, next-run-only browser behavior, high-threshold
219
+ downgrade, low-threshold promotion, keyboard slider operation, mobile
220
+ no-overflow layout, and canvas pointer targeting.
221
+ - GitHub Actions sync run `27515950105` deployed commit `a6f63e9` to the private
222
+ Hugging Face Space. The Space reported SHA
223
+ `a6f63e9a0b76315bb223a09a71f4c027a29877fb` and remained private.
224
+ - Authenticated private-Space verification passed: the root served the updated
225
+ shell, a synthetic image returned `optical sight` at `0.7` confidence with 3
226
+ parts, high threshold returned `photo` / `annotate`, low threshold returned
227
+ `three` / `three`, and no HTML field was present.
README.md CHANGED
@@ -123,23 +123,25 @@ Runtime flow:
123
 
124
  1. Browser encodes the uploaded photo and posts it to `/analyze_image`.
125
  2. Backend returns the validated mechanism JSON.
126
- 3. Browser posts the analysis to `/generate_scene` for a validated scene
127
- descriptor.
128
  4. Browser renders deterministic Three.js primitives when geometry is usable,
129
  or overlays text-only callouts on the uploaded photo when the model only has
130
  image-space annotations.
131
  5. A browser-side confidence slider controls the minimum analysis confidence
132
- needed for the 3D cutaway. Moving it re-renders from cached analysis data
133
- and does not call the model again.
134
  6. `/generate_scene` returns a validated
135
  `{ "renderer": "...", "render_mode": "...", "analysis": ... }` descriptor
136
- instead of model-authored HTML.
 
 
137
 
138
  The shell uses Chakra Petch and Fira Code from Bunny Fonts, an asymmetric
139
  63/37 viewport/readout split, a blueprint grid, amber/cyan instrument-panel
140
  colors, explicit Modal cold-start messaging, source-photo preview, and a
141
  play/pause control. The confidence slider defaults to 50%, matching the
142
- server's advisory fallback threshold, and the browser is authoritative for the
143
  visible render mode.
144
 
145
  The browser no longer injects model-authored HTML into the DOM. The model's
 
123
 
124
  1. Browser encodes the uploaded photo and posts it to `/analyze_image`.
125
  2. Backend returns the validated mechanism JSON.
126
+ 3. Browser posts the analysis plus the current confidence threshold to
127
+ `/generate_scene` for a validated scene descriptor.
128
  4. Browser renders deterministic Three.js primitives when geometry is usable,
129
  or overlays text-only callouts on the uploaded photo when the model only has
130
  image-space annotations.
131
  5. A browser-side confidence slider controls the minimum analysis confidence
132
+ needed for the 3D cutaway. Moving it only updates the next-run value; it does
133
+ not re-render cached analysis or call the model.
134
  6. `/generate_scene` returns a validated
135
  `{ "renderer": "...", "render_mode": "...", "analysis": ... }` descriptor
136
+ instead of model-authored HTML. The returned `render_mode` is the source of
137
+ truth for the visible renderer, with the browser only downgrading when
138
+ required data is missing.
139
 
140
  The shell uses Chakra Petch and Fira Code from Bunny Fonts, an asymmetric
141
  63/37 viewport/readout split, a blueprint grid, amber/cyan instrument-panel
142
  colors, explicit Modal cold-start messaging, source-photo preview, and a
143
  play/pause control. The confidence slider defaults to 50%, matching the
144
+ server's fallback threshold, and the generation route is authoritative for the
145
  visible render mode.
146
 
147
  The browser no longer injects model-authored HTML into the DOM. The model's