From 2e3c5eab6a72c183edfa824e3016dbcfb88ce7c7 Mon Sep 17 00:00:00 2001 From: cristhian aguilera Date: Tue, 3 Feb 2026 10:40:42 -0300 Subject: [PATCH] Improve voice control html interface --- .../dora_voice_control/web/api.py | 24 ++++ .../dora_voice_control/web/templates.py | 128 +++++++++++++++++- 2 files changed, 148 insertions(+), 4 deletions(-) diff --git a/dora_voice_control/dora_voice_control/web/api.py b/dora_voice_control/dora_voice_control/web/api.py index 33b3399..c49bfee 100644 --- a/dora_voice_control/dora_voice_control/web/api.py +++ b/dora_voice_control/dora_voice_control/web/api.py @@ -58,6 +58,30 @@ def create_api( except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.get("/api/scene") + def get_scene() -> dict: + """Get complete scene state including held object.""" + try: + detected = [obj.to_dict() for obj in scene.query(source="detected")] + config_objs = [obj.to_dict() for obj in scene.query(source="config")] + held_id = state.get_held_object_id() + held_obj = None + if held_id: + obj = scene.get(held_id) + if obj: + held_obj = obj.to_dict() + return { + "detected": detected, + "static": config_objs, + "held_object_id": held_id, + "held_object": held_obj, + "update_mode": scene._update_mode if hasattr(scene, "_update_mode") else "unknown", + "is_captured": scene.is_captured() if hasattr(scene, "is_captured") else False, + "total_count": scene.count(), + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + @app.get("/api/queue") def get_queue() -> list: """Get the command queue.""" diff --git a/dora_voice_control/dora_voice_control/web/templates.py b/dora_voice_control/dora_voice_control/web/templates.py index b1076f2..3a596b9 100644 --- a/dora_voice_control/dora_voice_control/web/templates.py +++ b/dora_voice_control/dora_voice_control/web/templates.py @@ -207,6 +207,60 @@ HTML_TEMPLATE = """ font-size: 11px; color: #888; } + .object-item.held { + border: 2px solid #00ff88; + background: #1a3a2e; + } + .object-item.held::before { + content: '\\1F91A'; + margin-right: 6px; + } + + /* Scene State */ + .scene-state { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 12px; + } + .held-object { + grid-column: span 2; + background: #1a3a2e; + border: 1px solid #00ff88; + padding: 12px; + border-radius: 6px; + } + .held-object.empty { + background: #1a1a2e; + border-color: #333; + } + .held-object .label { + font-size: 11px; + color: #00ff88; + text-transform: uppercase; + margin-bottom: 6px; + } + .held-object.empty .label { + color: #666; + } + .held-object .info { + display: flex; + gap: 15px; + flex-wrap: wrap; + } + .held-object .field { + display: flex; + flex-direction: column; + } + .held-object .field .key { + font-size: 10px; + color: #888; + } + .held-object .field .val { + font-size: 14px; + color: #fff; + font-weight: 500; + } /* Queue Display */ .queue-list { @@ -424,6 +478,27 @@ HTML_TEMPLATE = """ + +
+

Scene State

+
+
+
Update Mode
+
--
+
+
+
Captured
+
--
+
+
+
+
Held Object
+
+ Not holding any object +
+
+
+

Detected Objects

@@ -520,13 +595,53 @@ HTML_TEMPLATE = """ } } + let currentHeldId = null; + + async function updateScene() { + const data = await fetchJson('/api/scene'); + if (data.error) return; + + // Update scene mode + $('scene-mode').textContent = data.update_mode || '--'; + $('scene-mode').className = 'value ' + (data.update_mode === 'static' ? 'warn' : 'ok'); + + // Update captured status + $('scene-captured').textContent = data.is_captured ? 'Yes' : 'No'; + $('scene-captured').className = 'value ' + (data.is_captured ? 'ok' : 'warn'); + + // Update held object + currentHeldId = data.held_object_id; + const heldCard = $('held-object-card'); + const heldInfo = $('held-object-info'); + + if (data.held_object) { + const obj = data.held_object; + heldCard.className = 'held-object'; + const pos = obj.position_mm ? obj.position_mm.map(v => v.toFixed(0)).join(', ') : '--'; + heldInfo.innerHTML = + '
ID' + obj.id + '
' + + '
Type' + (obj.object_type || '?') + '
' + + '
Color' + (obj.color || '?') + '
' + + '
Size' + (obj.size || 'normal') + '
' + + '
Height' + (obj.height_mm || 0).toFixed(1) + 'mm
' + + '
Position[' + pos + ']
'; + } else if (data.held_object_id) { + heldCard.className = 'held-object'; + heldInfo.innerHTML = 'Holding ID: ' + data.held_object_id + ' (not in scene)'; + } else { + heldCard.className = 'held-object empty'; + heldInfo.innerHTML = 'Not holding any object'; + } + } + async function updateObjects() { - const data = await fetchJson('/api/objects'); + const data = await fetchJson('/api/scene'); if (data.error) return; const list = $('objects-list'); const detected = data.detected || []; const staticObjs = data.static || []; + const heldId = data.held_object_id; if (detected.length === 0 && staticObjs.length === 0) { list.innerHTML = '
No objects detected
'; @@ -543,12 +658,14 @@ HTML_TEMPLATE = """ const colorClass = obj.color || 'white'; const conf = obj.confidence ? (obj.confidence * 100).toFixed(0) + '%' : ''; const size = obj.size || ''; - return '
' + + const isHeld = obj.id === heldId; + const stackInfo = obj.on_top_of ? ' on:' + obj.on_top_of : ''; + return '
' + '' + (obj.object_type || '?') + '' + '' + (obj.color || '?') + '' + '' + size + '' + '' + conf + '' + - '[' + pos + ']' + + '[' + pos + ']' + stackInfo + '' + '
'; }).join(''); } @@ -559,7 +676,8 @@ HTML_TEMPLATE = """ html += staticObjs.map(obj => { const pos = obj.position_mm ? obj.position_mm.map(v => v.toFixed(0)).join(', ') : '--'; const colorClass = obj.color || 'white'; - return '
' + + const isHeld = obj.id === heldId; + return '
' + '' + (obj.object_type || '?') + '' + '' + (obj.color || '?') + '' + '[' + pos + ']' + @@ -684,12 +802,14 @@ HTML_TEMPLATE = """ // Auto-refresh setInterval(updateStatus, 500); + setInterval(updateScene, 500); setInterval(updateObjects, 1000); setInterval(updateQueue, 500); setInterval(updateCamera, 100); // Initial load updateStatus(); + updateScene(); updateObjects(); updateQueue(); updateCamera();