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

+34
View File
@@ -17,6 +17,40 @@ export function createControls({ state: layerState, services, parts, source }) {
parts.selection.stopMissionZoomAnchor();
},
/**
* Snapshot loaded launches as plain JSON-safe records for the analyst
* query engine. On demand only (once per spoken query); [] while the
* layer is off.
* @param {number} [maxCount=Infinity] Most records to return.
* @returns {Array<Object>} {id, name, provider, status, launchSite, missionName, lat, lon, launchTimeMs, hoursUntil}.
*/
getAnalystRecords(maxCount = Infinity) {
if (!layerState._enabled) return [];
const now = Date.now();
const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
const result = [];
for (const launch of layerState._launches || []) {
if (result.length >= maxCount) break;
const at = Date.parse(launch.launchTime);
result.push({
id: launch.id,
name: text(launch.name),
provider: text(launch.provider),
status: text(launch.status),
launchSite: text(launch.launchSite),
missionName: text(launch.missionName),
lat: launch.lat,
lon: launch.lon,
launchTimeMs: Number.isFinite(at) ? at : null,
// Negative for launches already flown.
hoursUntil: Number.isFinite(at)
? Math.round(((at - now) / 3_600_000) * 10) / 10
: null,
});
}
return result;
},
getStats() {
return {
count: layerState._count,