mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24: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,111 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Summarize voice turn spans from the Realtime debug log.
|
||||
*
|
||||
* Every closed voice turn writes one `turn.span` record (see
|
||||
* src/voice/turnMetrics.js). This prints each span and p50/p90 figures for
|
||||
* end-of-speech → first audio, the longest silence, tool time, preamble rate,
|
||||
* spoken words and hedge hits.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/voice-turn-spans.mjs [log.jsonl] [--since <ISO time>] [--session <id>] [--json]
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const args = process.argv.slice(2);
|
||||
const option = (flag) => {
|
||||
const index = args.indexOf(flag);
|
||||
return index >= 0 ? args[index + 1] : null;
|
||||
};
|
||||
const positional = args.filter(
|
||||
(arg, index) => !arg.startsWith('--') && !args[index - 1]?.startsWith('--'),
|
||||
);
|
||||
const logPath =
|
||||
positional[0] || path.join(root, '.gev-logs', 'realtime-conversations.jsonl');
|
||||
const since = option('--since') ? Date.parse(option('--since')) : null;
|
||||
const session = option('--session');
|
||||
|
||||
const spans = [];
|
||||
for (const line of fs.readFileSync(logPath, 'utf8').split('\n')) {
|
||||
if (!line.includes('"turn.span"')) continue;
|
||||
let record;
|
||||
try {
|
||||
record = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (record.event !== 'turn.span') continue;
|
||||
if (since && Date.parse(record.timestamp) < since) continue;
|
||||
if (session && record.sessionId !== session) continue;
|
||||
spans.push({
|
||||
at: record.timestamp,
|
||||
session: record.sessionId,
|
||||
...record.payload,
|
||||
});
|
||||
}
|
||||
|
||||
const quantile = (values, q) => {
|
||||
const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
|
||||
if (!sorted.length) return null;
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))];
|
||||
};
|
||||
const withTools = spans.filter((span) => span.tools?.length);
|
||||
const slowToolTurns = withTools.filter((span) =>
|
||||
span.tools.some((tool) => tool.ms >= 700),
|
||||
);
|
||||
const summary = {
|
||||
turns: spans.length,
|
||||
toolTurns: withTools.length,
|
||||
firstAudioMs: {
|
||||
p50: quantile(spans.map((s) => s.first_audio_ms), 0.5),
|
||||
p90: quantile(spans.map((s) => s.first_audio_ms), 0.9),
|
||||
},
|
||||
maxSilenceMs: {
|
||||
p50: quantile(spans.map((s) => s.max_silence_ms), 0.5),
|
||||
p90: quantile(spans.map((s) => s.max_silence_ms), 0.9),
|
||||
},
|
||||
toolMs: {
|
||||
p50: quantile(withTools.flatMap((s) => s.tools.map((t) => t.ms)), 0.5),
|
||||
p90: quantile(withTools.flatMap((s) => s.tools.map((t) => t.ms)), 0.9),
|
||||
},
|
||||
preambleRate: withTools.length
|
||||
? withTools.filter((s) => s.preamble).length / withTools.length
|
||||
: null,
|
||||
slowToolPreambleOrProgressRate: slowToolTurns.length
|
||||
? slowToolTurns.filter((s) => s.preamble || s.progress_lines > 0).length /
|
||||
slowToolTurns.length
|
||||
: null,
|
||||
answerWords: {
|
||||
p50: quantile(spans.map((s) => s.answer_words), 0.5),
|
||||
p90: quantile(spans.map((s) => s.answer_words), 0.9),
|
||||
},
|
||||
spokenWordsTotal: spans.reduce((sum, s) => sum + (s.spoken_words || 0), 0),
|
||||
hedgeHits: spans.reduce((sum, s) => sum + (s.hedge_hits?.length || 0), 0),
|
||||
};
|
||||
|
||||
if (args.includes('--json')) {
|
||||
console.log(JSON.stringify({ summary, spans }, null, 2));
|
||||
} else {
|
||||
for (const span of spans) {
|
||||
const tools = (span.tools || [])
|
||||
.map((tool) => `${tool.name}:${tool.ms}ms`)
|
||||
.join(',');
|
||||
console.log(
|
||||
[
|
||||
span.at,
|
||||
`first=${span.first_audio_ms ?? '-'}ms`,
|
||||
`maxSilence=${span.max_silence_ms ?? '-'}ms`,
|
||||
`preamble=${span.preamble ? 'y' : 'n'}`,
|
||||
`progress=${span.progress_lines}`,
|
||||
`words=${span.answer_words}/${span.spoken_words}`,
|
||||
`hedges=${span.hedge_hits?.length || 0}`,
|
||||
tools || '(no tools)',
|
||||
span.answer_text ? `"${span.answer_text.slice(0, 80)}"` : '',
|
||||
].join(' '),
|
||||
);
|
||||
}
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
}
|
||||
Reference in new issue
Block a user