jasondo OpenAI Codex commited on
Commit
a6f63e9
·
1 Parent(s): b389979

Enforce confidence threshold during generation

Browse files

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

Files changed (5) hide show
  1. AGENTS.md +6 -0
  2. app.py +15 -5
  3. index.html +18 -24
  4. snap2sim/backend.py +2 -2
  5. snap2sim/schema.py +34 -10
AGENTS.md CHANGED
@@ -278,6 +278,12 @@ technical cutaway animation.
278
  returned real Modal analysis for `Target Reticle` at `0.7` confidence with 3
279
  parts, and `/generate_scene` returned `renderer: three`, `render_mode: three`,
280
  and no HTML field.
 
 
 
 
 
 
281
 
282
  ## Next Work
283
 
 
278
  returned real Modal analysis for `Target Reticle` at `0.7` confidence with 3
279
  parts, and `/generate_scene` returned `renderer: three`, `render_mode: three`,
280
  and no HTML field.
281
+ - Implemented the `FEATURE.md` confidence-threshold re-spec on June 14, 2026:
282
+ the slider is enabled before the first upload, sends `confidence_threshold`
283
+ only with `/generate_scene`, no longer re-renders cached analysis on slider
284
+ movement, and the server clamps/coerces the threshold before selecting
285
+ `render_mode`. The browser now trusts the server's `render_mode` and only
286
+ downgrades when geometry or annotation data is missing.
287
 
288
  ## Next Work
289
 
app.py CHANGED
@@ -17,6 +17,7 @@ from fastapi.responses import JSONResponse
17
  from PIL import Image, UnidentifiedImageError
18
 
19
  from snap2sim.backend import InferenceClient, Settings
 
20
 
21
  try:
22
  from gradio import Server
@@ -99,13 +100,19 @@ def analyze_image_http(payload: dict[str, Any]) -> dict[str, Any]:
99
 
100
 
101
  @app.api(name="generate_scene")
102
- def generate_scene_api(analysis: dict[str, Any]) -> dict[str, Any]:
103
- return _generate_scene(analysis)
 
 
 
104
 
105
 
106
  @app.post("/generate_scene")
107
  def generate_scene_http(payload: dict[str, Any]) -> dict[str, Any]:
108
- return _generate_scene(payload.get("analysis") or {})
 
 
 
109
 
110
 
111
  def _analyze_image(image_base64: str) -> dict[str, Any]:
@@ -113,8 +120,11 @@ def _analyze_image(image_base64: str) -> dict[str, Any]:
113
  return InferenceClient(Settings()).analyze_image(image)
114
 
115
 
116
- def _generate_scene(analysis: dict[str, Any]) -> dict[str, Any]:
117
- return InferenceClient(Settings()).generate_scene(analysis)
 
 
 
118
 
119
 
120
  def _decode_image(image_base64: str) -> Image.Image:
 
17
  from PIL import Image, UnidentifiedImageError
18
 
19
  from snap2sim.backend import InferenceClient, Settings
20
+ from snap2sim.schema import normalize_confidence_threshold
21
 
22
  try:
23
  from gradio import Server
 
100
 
101
 
102
  @app.api(name="generate_scene")
103
+ def generate_scene_api(
104
+ analysis: dict[str, Any],
105
+ confidence_threshold: float | None = None,
106
+ ) -> dict[str, Any]:
107
+ return _generate_scene(analysis, confidence_threshold)
108
 
109
 
110
  @app.post("/generate_scene")
111
  def generate_scene_http(payload: dict[str, Any]) -> dict[str, Any]:
112
+ return _generate_scene(
113
+ payload.get("analysis") or {},
114
+ payload.get("confidence_threshold"),
115
+ )
116
 
117
 
118
  def _analyze_image(image_base64: str) -> dict[str, Any]:
 
120
  return InferenceClient(Settings()).analyze_image(image)
121
 
122
 
123
+ def _generate_scene(analysis: dict[str, Any], threshold: Any = None) -> dict[str, Any]:
124
+ return InferenceClient(Settings()).generate_scene(
125
+ analysis,
126
+ normalize_confidence_threshold(threshold),
127
+ )
128
 
129
 
130
  def _decode_image(image_base64: str) -> Image.Image:
index.html CHANGED
@@ -592,7 +592,7 @@
592
  <button id="resetViewButton" class="tool-button" type="button" disabled>Reset view</button>
593
  <label class="threshold-control" for="confidenceThreshold">
594
  <span>Min confidence</span>
595
- <input id="confidenceThreshold" class="threshold-slider" type="range" min="0" max="1" step="0.05" value="0.5" aria-describedby="thresholdValue" disabled>
596
  <span id="thresholdValue" class="threshold-value" aria-live="polite">50%</span>
597
  </label>
598
  </div>
@@ -672,7 +672,6 @@
672
  let paused = false;
673
  let requestBusy = false;
674
  let coldStartTimer = 0;
675
- let thresholdRenderTimer = 0;
676
  let fallbackRuntime = null;
677
  let currentPreviewUrl = "";
678
  let confidenceThreshold = DEFAULT_CONFIDENCE_THRESHOLD;
@@ -721,7 +720,6 @@
721
  });
722
  confidenceThresholdInput.addEventListener("input", () => {
723
  updateConfidenceThreshold();
724
- scheduleThresholdRender();
725
  });
726
 
727
  async function runPipeline(file) {
@@ -746,7 +744,10 @@
746
  populateAnalysis(analysis);
747
 
748
  setStatus("RENDERING CUTAWAY...");
749
- const scenePayload = await postJson("/generate_scene", { analysis });
 
 
 
750
  renderScenePayload(scenePayload);
751
  } catch (error) {
752
  window.clearTimeout(coldStartTimer);
@@ -766,7 +767,6 @@
766
  playButton.textContent = "Play";
767
  playButton.disabled = true;
768
  resetViewButton.disabled = true;
769
- confidenceThresholdInput.disabled = true;
770
  viewportHint.hidden = true;
771
  retryButton.hidden = true;
772
  window.lastAnalysis = null;
@@ -808,7 +808,7 @@
808
  window.lastAnalysis = analysis;
809
  window.lastScenePayload = payload;
810
  }
811
- const renderMode = chooseRenderMode(analysis);
812
  if (renderMode === "three" && hasUsableGeometry(analysis)) {
813
  buildDeterministicScene(analysis);
814
  } else if (renderMode === "annotate" && hasAnnotations(analysis)) {
@@ -818,15 +818,18 @@
818
  }
819
  }
820
 
821
- function chooseRenderMode(analysis) {
822
- const confidence = typeof analysis.confidence === "number" ? analysis.confidence : 1;
823
- if (hasAnnotations(analysis) && (confidence < confidenceThreshold || !hasUsableGeometry(analysis))) {
824
- return "annotate";
825
- }
826
- if (hasUsableGeometry(analysis) && confidence >= confidenceThreshold) {
827
- return "three";
 
 
 
828
  }
829
- if (hasAnnotations(analysis)) {
830
  return "annotate";
831
  }
832
  return "unavailable";
@@ -1260,7 +1263,7 @@
1260
  requestBusy = active;
1261
  progress.classList.toggle("active", active);
1262
  uploadButton.disabled = active;
1263
- confidenceThresholdInput.disabled = active || !window.lastScenePayload;
1264
  }
1265
 
1266
  function setStatus(message, error) {
@@ -1276,15 +1279,6 @@
1276
  thresholdValue.textContent = Math.round(confidenceThreshold * 100) + "%";
1277
  }
1278
 
1279
- function scheduleThresholdRender() {
1280
- window.clearTimeout(thresholdRenderTimer);
1281
- thresholdRenderTimer = window.setTimeout(() => {
1282
- if (!requestBusy && window.lastScenePayload) {
1283
- renderScenePayload(window.lastScenePayload);
1284
- }
1285
- }, 90);
1286
- }
1287
-
1288
  function revealScan() {
1289
  scanLine.classList.remove("active");
1290
  void scanLine.offsetWidth;
 
592
  <button id="resetViewButton" class="tool-button" type="button" disabled>Reset view</button>
593
  <label class="threshold-control" for="confidenceThreshold">
594
  <span>Min confidence</span>
595
+ <input id="confidenceThreshold" class="threshold-slider" type="range" min="0" max="1" step="0.05" value="0.5" aria-describedby="thresholdValue">
596
  <span id="thresholdValue" class="threshold-value" aria-live="polite">50%</span>
597
  </label>
598
  </div>
 
672
  let paused = false;
673
  let requestBusy = false;
674
  let coldStartTimer = 0;
 
675
  let fallbackRuntime = null;
676
  let currentPreviewUrl = "";
677
  let confidenceThreshold = DEFAULT_CONFIDENCE_THRESHOLD;
 
720
  });
721
  confidenceThresholdInput.addEventListener("input", () => {
722
  updateConfidenceThreshold();
 
723
  });
724
 
725
  async function runPipeline(file) {
 
744
  populateAnalysis(analysis);
745
 
746
  setStatus("RENDERING CUTAWAY...");
747
+ const scenePayload = await postJson("/generate_scene", {
748
+ analysis,
749
+ confidence_threshold: confidenceThreshold
750
+ });
751
  renderScenePayload(scenePayload);
752
  } catch (error) {
753
  window.clearTimeout(coldStartTimer);
 
767
  playButton.textContent = "Play";
768
  playButton.disabled = true;
769
  resetViewButton.disabled = true;
 
770
  viewportHint.hidden = true;
771
  retryButton.hidden = true;
772
  window.lastAnalysis = null;
 
808
  window.lastAnalysis = analysis;
809
  window.lastScenePayload = payload;
810
  }
811
+ const renderMode = chooseRenderMode(payload, analysis);
812
  if (renderMode === "three" && hasUsableGeometry(analysis)) {
813
  buildDeterministicScene(analysis);
814
  } else if (renderMode === "annotate" && hasAnnotations(analysis)) {
 
818
  }
819
  }
820
 
821
+ function chooseRenderMode(payload, analysis) {
822
+ const serverMode = payload && typeof payload.render_mode === "string"
823
+ ? payload.render_mode
824
+ : analysis && typeof analysis.render_mode === "string"
825
+ ? analysis.render_mode
826
+ : "unavailable";
827
+ if (serverMode === "three") {
828
+ if (hasUsableGeometry(analysis)) return "three";
829
+ if (hasAnnotations(analysis)) return "annotate";
830
+ return "unavailable";
831
  }
832
+ if (serverMode === "annotate" && hasAnnotations(analysis)) {
833
  return "annotate";
834
  }
835
  return "unavailable";
 
1263
  requestBusy = active;
1264
  progress.classList.toggle("active", active);
1265
  uploadButton.disabled = active;
1266
+ confidenceThresholdInput.disabled = active;
1267
  }
1268
 
1269
  function setStatus(message, error) {
 
1279
  thresholdValue.textContent = Math.round(confidenceThreshold * 100) + "%";
1280
  }
1281
 
 
 
 
 
 
 
 
 
 
1282
  function revealScan() {
1283
  scanLine.classList.remove("active");
1284
  void scanLine.offsetWidth;
snap2sim/backend.py CHANGED
@@ -44,9 +44,9 @@ class InferenceClient:
44
 
45
  return validate_analysis(dict(EXAMPLE_ANALYSIS))
46
 
47
- def generate_scene(self, analysis: dict[str, Any]) -> dict[str, Any]:
48
  valid_analysis = validate_analysis(analysis)
49
- render_mode = select_render_mode(valid_analysis)
50
  renderer = "three" if render_mode == "three" else "photo"
51
  return {"renderer": renderer, "render_mode": render_mode, "analysis": valid_analysis}
52
 
 
44
 
45
  return validate_analysis(dict(EXAMPLE_ANALYSIS))
46
 
47
+ def generate_scene(self, analysis: dict[str, Any], threshold: Any = None) -> dict[str, Any]:
48
  valid_analysis = validate_analysis(analysis)
49
+ render_mode = select_render_mode(valid_analysis, threshold)
50
  renderer = "three" if render_mode == "three" else "photo"
51
  return {"renderer": renderer, "render_mode": render_mode, "analysis": valid_analysis}
52
 
snap2sim/schema.py CHANGED
@@ -2,6 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  from typing import Any, Literal
6
 
7
  MotionType = Literal["rotate", "translate", "oscillate", "static"]
@@ -216,24 +217,47 @@ _RENDER_MODES = {"three", "annotate", "unavailable"}
216
  DEFAULT_CONFIDENCE_THRESHOLD = 0.5
217
 
218
 
219
- def select_render_mode(analysis: dict[str, Any]) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  """Pick the safest advisory renderer for a validated analysis payload."""
 
221
  explicit_mode = analysis.get("render_mode")
222
- if explicit_mode in _RENDER_MODES:
223
- return str(explicit_mode)
224
 
225
  parts = analysis.get("parts") if isinstance(analysis, dict) else []
226
  has_geometry = any(isinstance(part, dict) and isinstance(part.get("geometry"), dict) for part in parts)
227
  has_annotation = any(isinstance(part, dict) and isinstance(part.get("annotation"), dict) for part in parts)
228
  confidence = analysis.get("confidence", 1)
229
- low_confidence = _is_number(confidence) and float(confidence) < DEFAULT_CONFIDENCE_THRESHOLD
230
  if has_annotation and (low_confidence or not has_geometry):
231
- return "annotate"
232
- if has_geometry and not low_confidence:
233
- return "three"
234
- if has_annotation:
235
- return "annotate"
236
- return "unavailable"
 
 
 
 
 
 
 
 
 
237
 
238
 
239
  def validate_analysis(payload: dict[str, Any]) -> dict[str, Any]:
 
2
 
3
  from __future__ import annotations
4
 
5
+ import math
6
  from typing import Any, Literal
7
 
8
  MotionType = Literal["rotate", "translate", "oscillate", "static"]
 
217
  DEFAULT_CONFIDENCE_THRESHOLD = 0.5
218
 
219
 
220
+ def normalize_confidence_threshold(threshold: Any = DEFAULT_CONFIDENCE_THRESHOLD) -> float:
221
+ if isinstance(threshold, bool) or threshold is None:
222
+ return DEFAULT_CONFIDENCE_THRESHOLD
223
+ try:
224
+ value = float(threshold)
225
+ except (TypeError, ValueError):
226
+ return DEFAULT_CONFIDENCE_THRESHOLD
227
+ if not math.isfinite(value):
228
+ return DEFAULT_CONFIDENCE_THRESHOLD
229
+ return max(0.0, min(1.0, value))
230
+
231
+
232
+ def select_render_mode(
233
+ analysis: dict[str, Any],
234
+ threshold: Any = DEFAULT_CONFIDENCE_THRESHOLD,
235
+ ) -> str:
236
  """Pick the safest advisory renderer for a validated analysis payload."""
237
+ threshold = normalize_confidence_threshold(threshold)
238
  explicit_mode = analysis.get("render_mode")
 
 
239
 
240
  parts = analysis.get("parts") if isinstance(analysis, dict) else []
241
  has_geometry = any(isinstance(part, dict) and isinstance(part.get("geometry"), dict) for part in parts)
242
  has_annotation = any(isinstance(part, dict) and isinstance(part.get("annotation"), dict) for part in parts)
243
  confidence = analysis.get("confidence", 1)
244
+ low_confidence = _is_number(confidence) and float(confidence) < threshold
245
  if has_annotation and (low_confidence or not has_geometry):
246
+ mode = "annotate"
247
+ elif has_geometry and not low_confidence:
248
+ mode = "three"
249
+ elif has_annotation:
250
+ mode = "annotate"
251
+ else:
252
+ mode = "unavailable"
253
+ if explicit_mode in _RENDER_MODES:
254
+ return _lower_render_mode(str(explicit_mode), mode)
255
+ return mode
256
+
257
+
258
+ def _lower_render_mode(left: str, right: str) -> str:
259
+ rank = {"unavailable": 0, "annotate": 1, "three": 2}
260
+ return left if rank[left] <= rank[right] else right
261
 
262
 
263
  def validate_analysis(payload: dict[str, Any]) -> dict[str, Any]: