Spaces:
Running
Running
jasondo OpenAI Codex commited on
Commit ·
a6f63e9
1
Parent(s): b389979
Enforce confidence threshold during generation
Browse filesCo-authored-by: OpenAI Codex <codex@openai.com>
- AGENTS.md +6 -0
- app.py +15 -5
- index.html +18 -24
- snap2sim/backend.py +2 -2
- 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(
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
|
| 106 |
@app.post("/generate_scene")
|
| 107 |
def generate_scene_http(payload: dict[str, Any]) -> dict[str, Any]:
|
| 108 |
-
return _generate_scene(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 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", {
|
|
|
|
|
|
|
|
|
|
| 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
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) <
|
| 230 |
if has_annotation and (low_confidence or not has_geometry):
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]:
|