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-no-verbatim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents-plugin-elevenlabs': minor
---

Add ElevenLabs STT `noVerbatim` option.
25 changes: 23 additions & 2 deletions plugins/elevenlabs/src/stt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface STTOptions {
httpSession?: STTHTTPSession;
modelId?: ElevenLabsSTTModels | string;
keyterms?: string[];
noVerbatim?: boolean;
}

interface ResolvedSTTOptions {
Expand All @@ -71,6 +72,7 @@ interface ResolvedSTTOptions {
sampleRate: STTRealtimeSampleRates;
serverVad?: VADOptions | null;
keyterms?: string[];
noVerbatim: boolean;
}

export interface STTRecognizeOptions {
Expand Down Expand Up @@ -234,6 +236,7 @@ export class STT extends stt.STT {
includeTimestamps,
modelId,
keyterms: opts.keyterms,
noVerbatim: opts.noVerbatim ?? false,
};
this.#session = opts.httpSession ?? {};
}
Expand Down Expand Up @@ -347,6 +350,9 @@ export class STT extends stt.STT {
form.append('keyterms', keyterm);
}
}
if (this.#opts.noVerbatim) {
form.append('no_verbatim', 'true');
}

try {
const fetchFn = this.#session.fetch ?? fetch;
Expand Down Expand Up @@ -428,6 +434,7 @@ export class STT extends stt.STT {
tagAudioEvents?: boolean;
serverVad?: VADOptions | null;
keyterms?: string[];
noVerbatim?: boolean;
}): void {
if (opts.tagAudioEvents !== undefined) {
this.#opts.tagAudioEvents = opts.tagAudioEvents;
Expand All @@ -441,10 +448,14 @@ export class STT extends stt.STT {
this.#opts.keyterms = opts.keyterms;
}

if (opts.noVerbatim !== undefined) {
this.#opts.noVerbatim = opts.noVerbatim;
}

for (const ref of this.#streams) {
const stream = ref.deref();
if (stream) {
stream.updateOptions({ serverVad: opts.serverVad });
stream.updateOptions({ serverVad: opts.serverVad, noVerbatim: opts.noVerbatim });
} else {
this.#streams.delete(ref);
}
Expand Down Expand Up @@ -494,13 +505,19 @@ export class SpeechStream extends stt.SpeechStream {
);
}

updateOptions(opts: { serverVad?: VADOptions | null }): void {
updateOptions(opts: { serverVad?: VADOptions | null; noVerbatim?: boolean }): void {
if (opts.serverVad !== undefined) {
this.#opts.serverVad = opts.serverVad;
if (!this.#reconnectEvent.done) {
this.#reconnectEvent.resolve();
}
}
if (opts.noVerbatim !== undefined) {
this.#opts.noVerbatim = opts.noVerbatim;
if (!this.#reconnectEvent.done) {
this.#reconnectEvent.resolve();
}
}
}

#onAudioDurationReport(duration: number): void {
Expand Down Expand Up @@ -688,6 +705,10 @@ export class SpeechStream extends stt.SpeechStream {
params.push('include_timestamps=true');
}

if (this.#opts.noVerbatim) {
params.push('no_verbatim=true');
}
Comment on lines +708 to +710

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 keyterms not propagated to streaming WebSocket — pre-existing asymmetry

The keyterms option is only used in batch mode (#recognizeImpl at line 348-351) and is NOT added to the WebSocket URL params in #connectWs. In contrast, noVerbatim is correctly sent in both batch and streaming modes. If ElevenLabs' realtime API supports keyterms, this would be a pre-existing gap unrelated to this PR. The updateOptions method also doesn't propagate keyterms changes to streams, consistent with it being batch-only.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


const baseURL = this.#opts.baseURL.replace('https://', 'wss://').replace('http://', 'ws://');
const wsUrl = `${baseURL}/speech-to-text/realtime?${params.join('&')}`;
const headers = { [AUTHORIZATION_HEADER]: this.#opts.apiKey };
Expand Down