- backend/coach: routing zu Sonnet (default) / Haiku / Groq Llama je nach sessionData.llmProvider. sort:latency für Anthropic-Modelle (-30..58% TTFB). - frontend: LlmProviderToggle (Sonnet/Haiku/Groq pills), llmProvider.ts Storage-Helper. sosStream.ts schickt llmProvider im /sos-session-Body. - bench: SosTtsBenchmark sammelt Marker (req->session, lyra-ttfb, lyra-done, tts-fired/headers/body/file, audio-loaded, first-audio); Output als console.table. - ops: backend/scripts/llm-bench.sh + Python-Variante für realistic SOS-Prompt. - speak-cartesia + speak-elevenlabs Endpoints (waren ungetracked, jetzt mit drin).
106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
// SOS+TTS Latenz-Benchmark.
|
|
//
|
|
// Eine BenchSession pro sendToLyra-Call. Aggregiert Timing-Marker aus
|
|
// sosStream + sosTtsQueue und druckt am Ende eine Tabelle ins Dev-Console.
|
|
//
|
|
// Marker-Reihenfolge im typischen Flow:
|
|
// t0 request-fired (sendToLyra start)
|
|
// t1 session-post-done (POST /sos-session resolved → sessionId da)
|
|
// t2 sse-first-chunk ("Lyra denkt fertig" — erstes Token)
|
|
// t3 sse-done (full text fertig)
|
|
// t4 tts-fetch-start (POST an /api/coach/speak-* fired)
|
|
// t5 tts-fetch-headers (response headers da, body kommt noch)
|
|
// t6 tts-body-done (kompletter Audio-Body geladen — DAS ist der Bottleneck)
|
|
// t7 tts-file-written (base64 → File geschrieben)
|
|
// t8 audio-loaded (Audio.Sound.createAsync resolved)
|
|
// t9 first-audio (erstes onPlaybackStatusUpdate mit isPlaying)
|
|
//
|
|
// Bottleneck-Diagnose:
|
|
// - (t6 - t5) groß → Body-Download dominiert. Cartesia's TTFB-Vorteil
|
|
// verpufft hier weil wir auf alles warten statt zu streamen.
|
|
// - (t9 - t8) groß → expo-av lädt langsam (file-codec-detect etc.)
|
|
|
|
export type BenchMarker =
|
|
| 'session-post-start'
|
|
| 'session-post-done'
|
|
| 'sse-first-chunk'
|
|
| 'sse-done'
|
|
| 'tts-fetch-start'
|
|
| 'tts-fetch-headers'
|
|
| 'tts-body-done'
|
|
| 'tts-file-written'
|
|
| 'audio-loaded'
|
|
| 'first-audio';
|
|
|
|
export type BenchOnMetric = (marker: BenchMarker, meta?: Record<string, unknown>) => void;
|
|
|
|
type MarkerEntry = {
|
|
marker: BenchMarker;
|
|
/** ms relativ zu t0 */
|
|
tRel: number;
|
|
meta?: Record<string, unknown>;
|
|
};
|
|
|
|
export class BenchSession {
|
|
readonly t0: number;
|
|
readonly provider: string;
|
|
readonly label: string;
|
|
private entries: MarkerEntry[] = [];
|
|
private printed = false;
|
|
|
|
constructor(opts: { provider: string; label?: string }) {
|
|
this.t0 = Date.now();
|
|
this.provider = opts.provider;
|
|
this.label = opts.label ?? 'sos-turn';
|
|
}
|
|
|
|
/** Bound version — kann direkt als onMetric weitergegeben werden. */
|
|
readonly mark: BenchOnMetric = (marker, meta) => {
|
|
if (this.printed) return;
|
|
this.entries.push({ marker, tRel: Date.now() - this.t0, meta });
|
|
};
|
|
|
|
/** Druckt eine kompakte Tabelle. Idempotent (nur 1x pro Session). */
|
|
print(extraNote?: string): void {
|
|
if (this.printed) return;
|
|
this.printed = true;
|
|
|
|
const get = (m: BenchMarker) => this.entries.find((e) => e.marker === m)?.tRel;
|
|
const fmt = (v: number | undefined) => (v == null ? '—' : `${v}ms`);
|
|
const diff = (a: BenchMarker, b: BenchMarker) => {
|
|
const va = get(a), vb = get(b);
|
|
return va != null && vb != null ? `${vb - va}ms` : '—';
|
|
};
|
|
|
|
const stages = {
|
|
provider: this.provider,
|
|
label: this.label,
|
|
'req→session': fmt(get('session-post-done')),
|
|
'lyra-ttfb': fmt(get('sse-first-chunk')),
|
|
'lyra-done': fmt(get('sse-done')),
|
|
'tts-fired': fmt(get('tts-fetch-start')),
|
|
'tts-ttfb (rel)': diff('tts-fetch-start', 'tts-fetch-headers'),
|
|
'tts-body (rel)': diff('tts-fetch-headers', 'tts-body-done'),
|
|
'tts-file (rel)': diff('tts-body-done', 'tts-file-written'),
|
|
'audio-load (rel)': diff('tts-file-written', 'audio-loaded'),
|
|
'first-audio': fmt(get('first-audio')),
|
|
'TOTAL → speak': fmt(get('first-audio')),
|
|
};
|
|
|
|
// Eine kompakte Zeile als console.log (für Logbox-Lesbarkeit) +
|
|
// console.table mit allen Markern (für strukturierte Inspektion).
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`[bench] ${this.provider} (${this.label})${extraNote ? ' ' + extraNote : ''}`,
|
|
stages,
|
|
);
|
|
// eslint-disable-next-line no-console
|
|
console.table(this.entries.map((e) => ({ marker: e.marker, tRel: e.tRel })));
|
|
}
|
|
|
|
/** Snapshot für UI-Overlays (Debug-Drawer etc.). */
|
|
snapshot(): { provider: string; label: string; entries: MarkerEntry[] } {
|
|
return { provider: this.provider, label: this.label, entries: [...this.entries] };
|
|
}
|
|
}
|