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 = """
+
+
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();