Skip to content

Commit 5849a72

Browse files
committed
Harden installed vision diagnostics
1 parent cab8be7 commit 5849a72

7 files changed

Lines changed: 795 additions & 46 deletions

File tree

app/camera-state.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export function cameraErrorMessage(error) {
2+
const name = String(error?.name || "").trim();
3+
const message = String(error?.message || error || "").trim();
4+
const normalized = `${name} ${message}`.toLowerCase();
5+
6+
if (
7+
name === "NotFoundError" ||
8+
normalized.includes("requested device not found") ||
9+
normalized.includes("device not found") ||
10+
normalized.includes("no camera")
11+
) {
12+
return "No camera device was found. Connect or enable a camera, then try again.";
13+
}
14+
15+
if (
16+
name === "NotAllowedError" ||
17+
name === "SecurityError" ||
18+
normalized.includes("permission denied") ||
19+
normalized.includes("not allowed")
20+
) {
21+
return "Camera permission was denied. Allow camera access for Iris, then try again.";
22+
}
23+
24+
if (name === "NotReadableError" || normalized.includes("could not start video source")) {
25+
return "The camera is already in use or unavailable. Close other camera apps, then try again.";
26+
}
27+
28+
return message || "Camera snapshot failed.";
29+
}

app/camera-state.test.mjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
import { cameraErrorMessage } from "./camera-state.js";
4+
5+
test("camera errors show no-camera guidance instead of raw DOM exceptions", () => {
6+
assert.equal(
7+
cameraErrorMessage(new DOMException("Requested device not found", "NotFoundError")),
8+
"No camera device was found. Connect or enable a camera, then try again."
9+
);
10+
});
11+
12+
test("camera permission errors show permission recovery guidance", () => {
13+
assert.equal(
14+
cameraErrorMessage(new DOMException("Permission denied", "NotAllowedError")),
15+
"Camera permission was denied. Allow camera access for Iris, then try again."
16+
);
17+
});
18+
19+
test("busy camera errors show close-other-apps guidance", () => {
20+
assert.equal(
21+
cameraErrorMessage(new DOMException("Could not start video source", "NotReadableError")),
22+
"The camera is already in use or unavailable. Close other camera apps, then try again."
23+
);
24+
});
25+
26+
test("unknown camera errors preserve useful details", () => {
27+
assert.equal(cameraErrorMessage(new Error("Camera driver crashed.")), "Camera driver crashed.");
28+
});

app/main.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "./attachment-state.js";
1212
import { requireTrustedBlobUrl } from "./attachment-url.js";
1313
import { latestBrowserPreview } from "./browser-preview.js";
14+
import { cameraErrorMessage } from "./camera-state.js";
1415
import {
1516
formatDynamicContextStatus,
1617
parseDynamicContextCommand
@@ -442,6 +443,9 @@ async function submitScreenAreaMessage() {
442443
if (thinking || speaking) {
443444
return;
444445
}
446+
await cancelActiveAsr();
447+
wakeCommandArmed = false;
448+
voiceLoop = false;
445449
const latencyTrace = new VoiceLatencyTrace();
446450
const prompt = elements.hudInput.value.trim() || defaultScreenPrompt;
447451
const turnStarted = performance.now();
@@ -457,6 +461,9 @@ async function submitScreenAreaMessage() {
457461
elements.hudOutput.textContent = response.text;
458462
rememberTurn("user", `[screen] ${prompt}`);
459463
rememberTurn("iris", response.text);
464+
if (response.diagnostic_path) {
465+
logVoice("screen_probe_diagnostic", response.diagnostic_path);
466+
}
460467
logVoice(
461468
"screen_probe_complete",
462469
`model_ms=${response.model_elapsed_ms}; total_ms=${Math.round(performance.now() - turnStarted)}`
@@ -1506,7 +1513,9 @@ elements.visionButton.addEventListener("click", () => {
15061513
return;
15071514
}
15081515
lookWithCamera().catch((error) => {
1509-
elements.hudOutput.textContent = String(error);
1516+
logVoice("camera_snapshot_error", String(error));
1517+
elements.hudOutput.textContent = cameraErrorMessage(error);
1518+
restartListeningIfReady();
15101519
});
15111520
});
15121521

@@ -1657,6 +1666,9 @@ async function readDocumentAttachment(file) {
16571666

16581667
async function lookWithCamera() {
16591668
const prompt = elements.hudInput.value.trim() || defaultCameraPrompt;
1669+
await cancelActiveAsr();
1670+
wakeCommandArmed = false;
1671+
voiceLoop = false;
16601672
await captureCameraSnapshot();
16611673
elements.hudInput.value = "";
16621674
resizeComposerInput();
@@ -1681,10 +1693,9 @@ async function captureCameraSnapshot() {
16811693
frameRate: { ideal: 5, max: 10 }
16821694
}
16831695
});
1684-
setImageAttachment(
1685-
await snapshotFromStream(stream),
1686-
"Camera snapshot attached. Type what you want Iris to inspect."
1687-
);
1696+
const snapshot = await snapshotFromStream(stream);
1697+
await saveCameraSnapshotDiagnostic(snapshot);
1698+
setImageAttachment(snapshot, "Camera snapshot captured. Thinking locally...");
16881699
} finally {
16891700
if (stream) {
16901701
for (const track of stream.getTracks()) {
@@ -1725,11 +1736,30 @@ async function snapshotFromStream(stream) {
17251736
return {
17261737
name: "camera-snapshot.jpg",
17271738
bytes: Array.from(new Uint8Array(buffer)),
1739+
width,
1740+
height,
17281741
previewUrl: createTrustedAttachmentObjectUrl(blob),
17291742
kindLabel: "Camera"
17301743
};
17311744
}
17321745

1746+
async function saveCameraSnapshotDiagnostic(snapshot) {
1747+
try {
1748+
const diagnostic = await call("save_camera_snapshot_diagnostic", {
1749+
imageBytes: snapshot.bytes,
1750+
width: snapshot.width,
1751+
height: snapshot.height
1752+
});
1753+
const imagePath = diagnostic.imagePath || diagnostic.image_path || "";
1754+
logVoice(
1755+
"camera_snapshot_diagnostic",
1756+
`bytes=${snapshot.bytes.length}; width=${snapshot.width}; height=${snapshot.height}; path=${imagePath}`
1757+
);
1758+
} catch (error) {
1759+
logVoice("camera_snapshot_diagnostic_error", String(error));
1760+
}
1761+
}
1762+
17331763
async function snapshotFromVideoFile(file) {
17341764
validateVideoSize(file);
17351765
const video = document.createElement("video");

0 commit comments

Comments
 (0)