feat(voice): voice core — honest layer reach, spoken summary with on-screen card, narration, point-and-ask

- Voice layer manifest (src/voice/layerManifest.js) generates the layer enums,
  aliases and analyst field hints; a test fails when a catalog layer has no entry.
- analyst_query validates fields, operators, units, values and scopes, counts up
  to 250,000 loaded records per layer, reports complete/partial/unanswered, tells
  LAYER_OFF / NOT_READY / FEED_UNAVAILABLE apart from zero, refuses mismatched
  follow-ups and never lets a superseded query replace follow-up memory. Seven
  more layers expose analyst records.
- Tool results carry a code-built say line and a display card; the voice card in
  the mic control shows captions, plan steps and the result
  (src/voice/resultDisplay.js is the one adapter). Slow tools narrate their
  current step out of band; holding Space while the assistant talks cuts it off.
  Turn spans go to the debug log; caption transcription is metered; the log
  omits what was said unless GEV_VOICE_LOG_CONTENT=1.
- Point and ask: the Space-down pointer snapshot resolves "this", "that one" and
  "around here" through 'pointer' targets, and "the second one" through
  referent:n from the last numbered list.
- Prompt states each policy once (instructions + tools 48.5k -> 46.7k chars).
- Voice benchmark harness (scripts/qa-voice-bench.mjs, scripts/voice-bench/)
  shares its phrase table and grading with scripts/qa-voice-routing.mjs.
This commit is contained in:
Bilawal Sidhu committed 2026-09-28 21:19:23 -05:00
1 parent e7707d9a0f
commit a19897ad15
97 files changed
+14343 -755

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
import * as Cesium from 'cesium';
import { resolvePickedContext } from '../data/pickedContext.js';
import { cameraPoseKey } from './pointerContext.js';
import * as defaultAnnotationResolver from '../annotations/annotationResolver.js';
/** "Around here" radius as a share of camera height, clamped to sane bounds. */
export function pointerRadiusKm(cameraHeightM) {
const heightKm = Number(cameraHeightM) / 1000;
if (!Number.isFinite(heightKm) || heightKm <= 0) return 50;
return Math.round(Math.min(400, Math.max(5, heightKm * 0.75)));
}
/**
* The Cesium side of point-and-ask: one entity pick and one depth-aware world
* pick at canvas CSS pixels. Called lazily by the pointer tracker (dwell and
* snapshot only), never per mouse move.
* @param {object} options
* @param {object} options.viewer Cesium viewer.
* @param {object} [options.dataManager] Layer lifecycle.
* @param {Function} [options.pickWorldFromScreen] Normalized-pixel world pick.
* @returns {(x: number, y: number) => {entity: object|null, ground: object|null, radiusKm: number}|null}
*/
export function createPointerPicker({
viewer,
dataManager = null,
pickWorldFromScreen = defaultAnnotationResolver.pickWorldFromScreen,
}) {
return function pickPointer(x, y) {
const scene = viewer?.scene;
const canvas = scene?.canvas;
if (!scene || !canvas) return null;
const width = canvas.clientWidth || canvas.width || 0;
const height = canvas.clientHeight || canvas.height || 0;
if (!width || !height) return null;
let picked = null;
try {
picked = scene.pick(new Cesium.Cartesian2(x, y));
} catch {
picked = null;
}
const entity = resolvePickedContext(picked, { dataManager });
let ground = null;
try {
const hit = pickWorldFromScreen(viewer, x / width, y / height);
if (hit && Number.isFinite(hit.lat) && Number.isFinite(hit.lon))
ground = {
lat: hit.lat,
lon: hit.lon,
heightM: Number.isFinite(hit.height) ? Math.round(hit.height) : null,
};
} catch {
ground = null;
}
return {
entity,
ground,
radiusKm: pointerRadiusKm(viewer.camera?.positionCartographic?.height),
cameraKey: cameraPoseKey(viewer),
};
};
}