Skip to content

Commit 89ec76b

Browse files
authored
Surface AssemblyAI language confidence metadata (#1543)
1 parent bfad336 commit 89ec76b

3 files changed

Lines changed: 154 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@livekit/agents-plugin-assemblyai': patch
3+
---
4+
5+
Surface AssemblyAI language confidence on speech event metadata.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
import { stt as sttLib } from '@livekit/agents';
5+
import { AudioFrame } from '@livekit/rtc-node';
6+
import { once } from 'node:events';
7+
import type { AddressInfo } from 'node:net';
8+
import { describe, expect, it } from 'vitest';
9+
import { WebSocketServer } from 'ws';
10+
import { STT } from './stt.js';
11+
12+
function makeFrame(samplesPerChannel = 800, sampleRate = 16000): AudioFrame {
13+
const data = new Int16Array(samplesPerChannel);
14+
data.fill(1);
15+
return new AudioFrame(data, sampleRate, 1, samplesPerChannel);
16+
}
17+
18+
async function startWebSocketServer() {
19+
const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 });
20+
await once(wss, 'listening');
21+
const address = wss.address() as AddressInfo;
22+
return { wss, baseUrl: `ws://127.0.0.1:${address.port}` };
23+
}
24+
25+
async function closeWebSocketServer(wss: WebSocketServer): Promise<void> {
26+
await new Promise<void>((resolve) => wss.close(() => resolve()));
27+
}
28+
29+
async function waitUntil(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
30+
const startedAt = Date.now();
31+
while (Date.now() - startedAt < timeoutMs) {
32+
if (predicate()) return;
33+
await new Promise((resolve) => setTimeout(resolve, 10));
34+
}
35+
throw new Error('timed out waiting for condition');
36+
}
37+
38+
async function collectUntilEnd(stream: sttLib.SpeechStream): Promise<sttLib.SpeechEvent[]> {
39+
const events: sttLib.SpeechEvent[] = [];
40+
for await (const event of stream) {
41+
events.push(event);
42+
if (event.type === sttLib.SpeechEventType.END_OF_SPEECH) break;
43+
}
44+
return events;
45+
}
46+
47+
describe('AssemblyAI STT metadata', () => {
48+
it('maps turn confidence fields onto speech data metadata', async () => {
49+
const { wss, baseUrl } = await startWebSocketServer();
50+
let requestUrl = '';
51+
52+
wss.on('connection', (ws, req) => {
53+
requestUrl = req.url ?? '';
54+
ws.on('message', () => {
55+
ws.send(
56+
JSON.stringify({
57+
type: 'Turn',
58+
transcript: 'hola mundo',
59+
utterance: 'hola mundo',
60+
end_of_turn: true,
61+
language_code: 'es',
62+
language_confidence: 0.94,
63+
words: [
64+
{ text: 'hola', start: 0, end: 200, confidence: 0.96 },
65+
{ text: 'mundo', start: 200, end: 500, confidence: 0.98 },
66+
],
67+
}),
68+
);
69+
});
70+
});
71+
72+
try {
73+
const assemblyai = new STT({
74+
apiKey: 'test-key',
75+
baseUrl,
76+
speechModel: 'u3-rt-pro',
77+
});
78+
const stream = assemblyai.stream({
79+
connOptions: { maxRetry: 0, retryIntervalMs: 1, timeoutMs: 1000 },
80+
});
81+
82+
await waitUntil(() => requestUrl !== '');
83+
84+
stream.pushFrame(makeFrame());
85+
stream.endInput();
86+
87+
const events = await collectUntilEnd(stream);
88+
stream.close();
89+
90+
expect(new URL(`ws://127.0.0.1${requestUrl}`).pathname).toBe('/v3/ws');
91+
expect(
92+
events
93+
.filter((event) => event.alternatives?.[0])
94+
.map((event) => ({
95+
type: event.type,
96+
language: event.alternatives?.[0]?.language,
97+
metadata: event.alternatives?.[0]?.metadata,
98+
})),
99+
).toEqual([
100+
{
101+
type: sttLib.SpeechEventType.INTERIM_TRANSCRIPT,
102+
language: 'es',
103+
metadata: {
104+
assemblyai: {
105+
languageConfidence: 0.94,
106+
},
107+
},
108+
},
109+
{
110+
type: sttLib.SpeechEventType.PREFLIGHT_TRANSCRIPT,
111+
language: 'es',
112+
metadata: {
113+
assemblyai: {
114+
languageConfidence: 0.94,
115+
},
116+
},
117+
},
118+
{
119+
type: sttLib.SpeechEventType.FINAL_TRANSCRIPT,
120+
language: 'es',
121+
metadata: {
122+
assemblyai: {
123+
languageConfidence: 0.94,
124+
},
125+
},
126+
},
127+
]);
128+
} finally {
129+
await closeWebSocketServer(wss);
130+
}
131+
});
132+
});

plugins/assemblyai/src/stt.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ interface StreamEventMessage {
4141
end_of_turn_confidence?: number;
4242
turn_is_formatted?: boolean;
4343
language_code?: string;
44+
language_confidence?: number;
4445
speaker_label?: string;
4546
words?: Array<{
4647
text?: string;
@@ -54,6 +55,18 @@ interface StreamEventMessage {
5455
session_duration_seconds?: number;
5556
}
5657

58+
function speechDataMetadata(data: StreamEventMessage): stt.SpeechData['metadata'] | undefined {
59+
const assemblyai: Record<string, number> = {};
60+
61+
if (typeof data.language_confidence === 'number') {
62+
assemblyai.languageConfidence = data.language_confidence;
63+
}
64+
65+
if (Object.keys(assemblyai).length === 0) return undefined;
66+
67+
return { assemblyai };
68+
}
69+
5770
export interface STTOptions {
5871
apiKey?: string;
5972
sampleRate: number;
@@ -525,6 +538,7 @@ export class SpeechStream extends stt.SpeechStream {
525538
const utterance = data.utterance ?? '';
526539
const transcript = data.transcript ?? '';
527540
const language = normalizeLanguage(data.language_code ?? 'en');
541+
const metadata = speechDataMetadata(data);
528542

529543
// Word timestamps are in milliseconds:
530544
// https://www.assemblyai.com/docs/api-reference/streaming-api/streaming-api#receive.receiveTurn.words
@@ -559,6 +573,7 @@ export class SpeechStream extends stt.SpeechStream {
559573
endTime,
560574
confidence,
561575
words: timedWords,
576+
...(metadata ? { metadata } : {}),
562577
},
563578
],
564579
});
@@ -586,6 +601,7 @@ export class SpeechStream extends stt.SpeechStream {
586601
endTime,
587602
confidence: utteranceConfidence,
588603
words: utteranceWords,
604+
...(metadata ? { metadata } : {}),
589605
},
590606
],
591607
});
@@ -606,6 +622,7 @@ export class SpeechStream extends stt.SpeechStream {
606622
endTime,
607623
confidence,
608624
words: timedWords,
625+
...(metadata ? { metadata } : {}),
609626
},
610627
],
611628
});

0 commit comments

Comments
 (0)