Skip to content

Commit d4edb58

Browse files
fix(voice): retain concrete language for turn detection (#2149)
Co-authored-by: rosetta-livekit-bot[bot] <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com>
1 parent 49a1167 commit d4edb58

3 files changed

Lines changed: 54 additions & 10 deletions

File tree

.changeset/green-mice-hear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@livekit/agents': patch
3+
---
4+
5+
Preserve concrete STT language hints when later transcripts report non-specific language codes.

agents/src/voice/audio_recognition.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44
import { Mutex } from '@livekit/mutex';
5-
import { AudioFrame, type ParticipantKind } from '@livekit/rtc-node';
5+
import type { AudioFrame, ParticipantKind } from '@livekit/rtc-node';
66
import { ThrowsPromise } from '@livekit/throws-transformer/throws';
77
import {
88
type Context,
@@ -67,6 +67,8 @@ import {
6767
// Maximum number of chat items included in the `lk.chat_ctx` attribute of the
6868
// `eou_detection` span (mirrors Python's `_EOU_MAX_HISTORY_TURNS`).
6969
const EOU_MAX_HISTORY_TURNS = 6;
70+
const MIN_LANGUAGE_DETECTION_LENGTH = 5;
71+
const NON_SPECIFIC_LANGUAGE_CODES = new Set(['auto', 'multi']);
7072

7173
export interface EndOfTurnInfo {
7274
/** The new transcript text from the user's speech. */
@@ -636,6 +638,16 @@ export class AudioRecognition {
636638
}
637639
}
638640

641+
private updateLastLanguage(language: LanguageCode | undefined, transcript: string): void {
642+
if (!language || NON_SPECIFIC_LANGUAGE_CODES.has(language)) {
643+
return;
644+
}
645+
646+
if (!this.lastLanguage || transcript.length > MIN_LANGUAGE_DETECTION_LENGTH) {
647+
this.lastLanguage = language;
648+
}
649+
}
650+
639651
async start(options?: {
640652
sttPipeline?: STTPipeline;
641653
turnDetectorStream?: BaseStreamingTurnDetectorStream;
@@ -1092,9 +1104,10 @@ export class AudioRecognition {
10921104

10931105
switch (ev.type) {
10941106
case SpeechEventType.FINAL_TRANSCRIPT:
1095-
const transcript = ev.alternatives?.[0]?.text;
1107+
const transcript = ev.alternatives?.[0]?.text ?? '';
10961108
const confidence = ev.alternatives?.[0]?.confidence ?? 0;
1097-
this.lastLanguage = ev.alternatives?.[0]?.language;
1109+
const language = ev.alternatives?.[0]?.language;
1110+
this.updateLastLanguage(language, transcript);
10981111

10991112
if (!transcript) {
11001113
// stt final transcript received but no transcript
@@ -1162,13 +1175,7 @@ export class AudioRecognition {
11621175
const preflightConfidence = ev.alternatives?.[0]?.confidence ?? 0;
11631176
const preflightLanguage = ev.alternatives?.[0]?.language;
11641177

1165-
const MIN_LANGUAGE_DETECTION_LENGTH = 5;
1166-
if (
1167-
!this.lastLanguage ||
1168-
(preflightLanguage && preflightTranscript.length > MIN_LANGUAGE_DETECTION_LENGTH)
1169-
) {
1170-
this.lastLanguage = preflightLanguage;
1171-
}
1178+
this.updateLastLanguage(preflightLanguage, preflightTranscript);
11721179

11731180
if (!preflightTranscript) {
11741181
return;

agents/src/voice/audio_recognition_turn_detection.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
MIN_SILENCE_DURATION_MS,
3131
type TurnDetectionEvent,
3232
} from '../inference/eot/base.js';
33+
import { type LanguageCode, asLanguageCode } from '../language.js';
3334
import { ChatContext } from '../llm/chat_context.js';
3435
import { initializeLogger } from '../log.js';
3536
import { Future } from '../utils.js';
@@ -55,6 +56,7 @@ interface RecognitionInternals {
5556
turnDetectorLatePredictionWarned: boolean;
5657
lastEmittedEotPrediction?: TurnDetectionEvent;
5758
lastSpeakingTime?: number;
59+
lastLanguage?: LanguageCode;
5860
audioTranscript: string;
5961
audioInterimTranscript: string;
6062
audioPreflightTranscript: string;
@@ -68,6 +70,7 @@ interface RecognitionInternals {
6870
runEOUDetection: (chatCtx: ChatContext, trigger?: 'vad' | 'stt' | 'manual') => void;
6971
createVadTask: (vad: VAD | undefined, signal: AbortSignal) => Promise<void>;
7072
checkVadSilenceRequirement: (detector?: _TurnDetector | BaseStreamingTurnDetector) => void;
73+
updateLastLanguage: (language: LanguageCode, transcript: string) => void;
7174
updateTurnDetector: (detector: _TurnDetector | BaseStreamingTurnDetector | undefined) => void;
7275
clearUserTurn: () => void;
7376
}
@@ -221,6 +224,35 @@ function flush(): Promise<void> {
221224
return new Promise((resolve) => setImmediate(resolve));
222225
}
223226

227+
describe('TestLanguageTracking', () => {
228+
it('does not replace a concrete language with a non-specific language', () => {
229+
const { internals } = makeRecognition();
230+
internals.lastLanguage = asLanguageCode('en');
231+
232+
internals.updateLastLanguage(asLanguageCode('multi'), 'ambiguous phrase');
233+
234+
expect(internals.lastLanguage).toBe(asLanguageCode('en'));
235+
});
236+
237+
it('does not initialize language with a non-specific language', () => {
238+
const { internals } = makeRecognition();
239+
internals.lastLanguage = undefined;
240+
241+
internals.updateLastLanguage(asLanguageCode('multi'), 'we');
242+
243+
expect(internals.lastLanguage).toBeUndefined();
244+
});
245+
246+
it('still updates concrete language after a long transcript', () => {
247+
const { internals } = makeRecognition();
248+
internals.lastLanguage = asLanguageCode('en');
249+
250+
internals.updateLastLanguage(asLanguageCode('fr'), 'bonjour');
251+
252+
expect(internals.lastLanguage).toBe(asLanguageCode('fr'));
253+
});
254+
});
255+
224256
/**
225257
* Drive `createVadTask` against a scripted VAD stream so VAD events flow
226258
* through the real handler. `feed()` resolves once the event has been processed

0 commit comments

Comments
 (0)