mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
- 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.
112 lines
3.7 KiB
JavaScript
112 lines
3.7 KiB
JavaScript
#!/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));
|
|
}
|