mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
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:
1 parent
e7707d9a0f
commit
a19897ad15
97 files changed
+14343
-755
No files matched your search
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* The one adapter from a tool result to what the voice card shows.
|
||||
*
|
||||
* Most tools carry a card-ready `display` ({title, lines, chips, notes,
|
||||
* sources}) from their speech builder. The analyst instead returns semantic
|
||||
* fields the engine owns (count, complete, scopeLabel, partial, and a
|
||||
* `display` of scope, window and caveat). This module turns those into the
|
||||
* card shape so every result renders the same way, and names the numbered
|
||||
* referents the card lists (the referent registry reads the same answer, so
|
||||
* "the second one" always means the second row on screen).
|
||||
*
|
||||
* Pure: no DOM, no Cesium.
|
||||
* @module voice/resultDisplay
|
||||
*/
|
||||
|
||||
import { voiceLayer } from './layerManifest.js';
|
||||
|
||||
const LAYER_NOUNS = Object.freeze({
|
||||
flights: 'aircraft',
|
||||
military: 'military aircraft',
|
||||
'local-adsb': 'aircraft',
|
||||
'ais-live-vessels': 'ships',
|
||||
satellites: 'satellites',
|
||||
'rocket-launches': 'launches',
|
||||
earthquakes: 'earthquakes',
|
||||
'local-firms': 'fires',
|
||||
});
|
||||
|
||||
/** Split "a; b" caveat text into separate notes. */
|
||||
function notesOf(...values) {
|
||||
return values
|
||||
.flatMap((value) => String(value || '').split(/;\s*/))
|
||||
.map((text) => text.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function nounFor(layerKeys) {
|
||||
const keys = [...new Set(layerKeys.filter(Boolean))];
|
||||
if (keys.length !== 1) return 'results';
|
||||
const key = keys[0];
|
||||
return LAYER_NOUNS[key] || voiceLayer(key)?.aliases?.[0] || 'results';
|
||||
}
|
||||
|
||||
function analystCard(result) {
|
||||
if (!result?.ok) return null;
|
||||
const keys = (result.coverage?.layersQueried || []).map(
|
||||
(layer) => layer.layerKey,
|
||||
);
|
||||
const count = Number(result.count) || 0;
|
||||
const floor = result.complete === false ? 'At least ' : '';
|
||||
const display = result.display || {};
|
||||
const unanswered = Array.isArray(result.unanswered) ? result.unanswered : [];
|
||||
return {
|
||||
display: {
|
||||
title:
|
||||
`${floor}${count.toLocaleString('en-US')} ${nounFor(keys)} ${result.scopeLabel || ''}`.trim(),
|
||||
// The precise scope, unless the title already says exactly that.
|
||||
lines: [
|
||||
display.scope !== result.scopeLabel ? display.scope : null,
|
||||
display.window,
|
||||
].filter(Boolean),
|
||||
chips: [
|
||||
...(result.feedState && result.feedState !== 'nominal'
|
||||
? [{ label: result.feedState }]
|
||||
: []),
|
||||
...(result.partial ? [{ label: 'partial' }] : []),
|
||||
],
|
||||
notes: [
|
||||
...notesOf(display.caveat, result.coverage?.note),
|
||||
...(unanswered.length
|
||||
? [`Not answered: ${unanswered.join(', ')}`]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
referents: Array.isArray(result.items) ? result.items : [],
|
||||
resultSet: true,
|
||||
};
|
||||
}
|
||||
|
||||
const ADAPTERS = Object.freeze({
|
||||
analyst_query: analystCard,
|
||||
});
|
||||
|
||||
/**
|
||||
* What the card shows for one result, or null when it shows nothing (a
|
||||
* silent lookup, a refusal, a cancelled call).
|
||||
* @param {string} name Tool name.
|
||||
* @param {object} result Tool result as the model received it.
|
||||
* @param {object} [args] Tool arguments.
|
||||
* @returns {{display: object, referents: Array<object>, resultSet?: boolean}|null}
|
||||
* `resultSet` marks a fresh answer set that replaces the numbered list
|
||||
* even when empty.
|
||||
*/
|
||||
export function presentResult(name, result, args = {}) {
|
||||
if (!result || typeof result !== 'object' || result.cancelled) return null;
|
||||
if (result.schedule === 'silent') return null;
|
||||
const adapter = ADAPTERS[name];
|
||||
if (adapter) {
|
||||
try {
|
||||
const card = adapter(result, args || {});
|
||||
if (card?.display?.title) return card;
|
||||
} catch {
|
||||
/* Presentation never breaks a result. */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!result.display?.title) return null;
|
||||
return {
|
||||
display: result.display,
|
||||
referents: Array.isArray(result.referents) ? result.referents : [],
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user