Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/elevenlabs-stt-previous-text.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents-plugin-elevenlabs': minor
---

Support previous text context for ElevenLabs realtime STT.
57 changes: 55 additions & 2 deletions plugins/elevenlabs/src/stt.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import { mergeFrames, stt as sttLib } from '@livekit/agents';
import { log, mergeFrames, stt as sttLib } from '@livekit/agents';
import { AudioFrame, AudioResampler } from '@livekit/rtc-node';
import { once } from 'node:events';
import { readFileSync } from 'node:fs';
import { type RequestListener, type Server, createServer } from 'node:http';
import type { AddressInfo } from 'node:net';
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { WebSocketServer } from 'ws';
import { STT } from './stt.js';

Expand Down Expand Up @@ -153,6 +153,59 @@ describe('ElevenLabs STT', () => {
expect(stt.capabilities.alignedTranscript).toBe('word');
});

it('keeps previousText for realtime model', async () => {
const { wss, baseURL } = await startWebSocketServer();
const receivedMessages: Record<string, unknown>[] = [];

wss.on('connection', (ws) => {
ws.on('message', (raw) => {
receivedMessages.push(JSON.parse(raw.toString()) as Record<string, unknown>);
});
});

try {
const eleven = new STT({
apiKey: 'test-key',
baseURL,
model: 'scribe_v2_realtime',
previousText: 'prior context',
});
const stream = eleven.stream();

await waitUntil(() => receivedMessages.length > 0);
stream.close();

expect(receivedMessages[0]).toMatchObject({
message_type: 'input_audio_chunk',
audio_base_64: '',
commit: false,
sample_rate: 16000,
previous_text: 'prior context',
});
} finally {
await closeWebSocketServer(wss);
}
});

it('ignores previousText for non-realtime model', () => {
const warn = vi.spyOn(log(), 'warn').mockImplementation(() => undefined);

try {
const eleven = new STT({
apiKey: 'test-key',
model: 'scribe_v2',
previousText: 'prior context',
});

expect(eleven.model).toBe('scribe_v2');
expect(warn).toHaveBeenCalledWith(
'`previousText` is only supported for Scribe v2 realtime model and will be ignored',
);
} finally {
warn.mockRestore();
}
});

it('sends batch recognition form fields and maps word metadata', async () => {
let request:
| {
Expand Down
27 changes: 27 additions & 0 deletions plugins/elevenlabs/src/stt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export interface STTOptions {
modelId?: ElevenLabsSTTModels | string;
keyterms?: string[];
noVerbatim?: boolean;
/**
* Preceding text context sent once on the first realtime audio chunk to improve transcription
* accuracy. Only supported for Scribe v2 realtime.
*/
previousText?: string;
}

interface ResolvedSTTOptions {
Expand All @@ -75,6 +80,7 @@ interface ResolvedSTTOptions {
serverVad?: VADOptions | null;
keyterms?: string[];
noVerbatim: boolean;
previousText: string | null;
}

export interface STTRecognizeOptions {
Expand Down Expand Up @@ -224,6 +230,14 @@ export class STT extends stt.STT {
log().warn('Server-side VAD is only supported for Scribe v2 realtime model');
}

let previousText = opts.previousText ?? null;
if (!useRealtime && previousText !== null) {
log().warn(
'`previousText` is only supported for Scribe v2 realtime model and will be ignored',
);
previousText = null;
}

const includeTimestamps = opts.includeTimestamps ?? false;
super({
streaming: useRealtime,
Expand All @@ -249,6 +263,7 @@ export class STT extends stt.STT {
modelId,
keyterms: opts.keyterms,
noVerbatim: opts.noVerbatim ?? false,
previousText,
};
this.#session = opts.httpSession ?? {};
}
Expand Down Expand Up @@ -546,6 +561,18 @@ export class SpeechStream extends stt.SpeechStream {

try {
ws = await this.#connectWs();
if (this.#opts.previousText) {
// Must be the first input_audio_chunk on the connection.
ws.send(
JSON.stringify({
message_type: 'input_audio_chunk',
audio_base_64: '',
commit: false,
sample_rate: this.#opts.sampleRate,
previous_text: this.#opts.previousText,
}),
);
}

const keepaliveTask = Task.from(async (controller) => {
while (!controller.signal.aborted) {
Expand Down
Loading