diff --git a/.changeset/scribe-enable-logging.md b/.changeset/scribe-enable-logging.md new file mode 100644 index 000000000..cf8f84888 --- /dev/null +++ b/.changeset/scribe-enable-logging.md @@ -0,0 +1,6 @@ +--- +"@elevenlabs/client": minor +"@elevenlabs/react": minor +--- + +Add `enableLogging` option (`boolean`) to the Scribe realtime API, available on `Scribe.connect` and the `useScribe` hook. Setting it to `false` sends `enable_logging=false` on the WebSocket URL, which runs the session in zero retention mode so history features are unavailable for it. Zero retention mode may only be used by enterprise customers. diff --git a/.changeset/scribe-session-started-enable-logging.md b/.changeset/scribe-session-started-enable-logging.md new file mode 100644 index 000000000..f4fc2d019 --- /dev/null +++ b/.changeset/scribe-session-started-enable-logging.md @@ -0,0 +1,5 @@ +--- +"@elevenlabs/types": minor +--- + +Rename `disable_logging` to `enable_logging` in the Scribe `session_started` config to match the field the server actually reports. `disable_logging` was never sent on the wire. diff --git a/packages/client/src/scribe/scribe.test.ts b/packages/client/src/scribe/scribe.test.ts index 52ad6e96d..2c8870811 100644 --- a/packages/client/src/scribe/scribe.test.ts +++ b/packages/client/src/scribe/scribe.test.ts @@ -1,4 +1,4 @@ -import { it, expect, describe, vi, beforeEach } from "vitest"; +import { it, expect, describe, vi, beforeEach, onTestFinished } from "vitest"; import { Server } from "mock-socket"; import type { Client } from "mock-socket"; import { @@ -18,6 +18,31 @@ const TEST_MODEL_ID = "scribe_v2_realtime"; const TEST_SESSION_ID = "test-session-id"; const PARTIAL_TRANSCRIPT_TEXT = "Hello, this is a partial"; const COMMITTED_TRANSCRIPT_TEXT = "Hello, this is a committed transcript."; +const SCRIBE_WS_URL = "wss://api.elevenlabs.io/v1/speech-to-text/realtime"; + +/** + * Starts a mock Scribe server and resolves with the query params the client + * connected with. The mock server matches on the URL without its query part, so + * inspecting the client URI is the only way to assert on the URI that was built. + * Call before connecting so the connection listener is already attached. + */ +function connectionQuery(): Promise { + const server = new Server(SCRIBE_WS_URL); + onTestFinished(() => server.close()); + + return new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error("timed out waiting for connection")), + 5000 + ); + onTestFinished(() => clearTimeout(timeout)); + + server.on("connection", socket => + resolve(new URL(socket.url).searchParams) + ); + server.on("error", reject); + }); +} describe("Scribe", () => { describe("WebSocket URI Building", () => { @@ -256,6 +281,41 @@ describe("Scribe", () => { server.close(); }); + it.each([ + { enableLogging: false, expected: "false" }, + { enableLogging: true, expected: "true" }, + ])( + "builds URI with enable_logging=$expected when enableLogging is $enableLogging", + async ({ enableLogging, expected }) => { + const query = connectionQuery(); + + const connection = Scribe.connect({ + token: TEST_TOKEN, + modelId: TEST_MODEL_ID, + audioFormat: AudioFormat.PCM_16000, + sampleRate: 16000, + enableLogging, + }); + onTestFinished(() => connection.close()); + + expect((await query).get("enable_logging")).toBe(expected); + } + ); + + it("omits enable_logging when enableLogging is not set", async () => { + const query = connectionQuery(); + + const connection = Scribe.connect({ + token: TEST_TOKEN, + modelId: TEST_MODEL_ID, + audioFormat: AudioFormat.PCM_16000, + sampleRate: 16000, + }); + onTestFinished(() => connection.close()); + + expect((await query).has("enable_logging")).toBe(false); + }); + it("accepts valid parameter values", () => { const server = new Server( "wss://api.elevenlabs.io/v1/speech-to-text/realtime?model_id=scribe_v2_realtime&token=sutkn_123&vad_silence_threshold_secs=1.5&vad_threshold=0.5&min_speech_duration_ms=100&min_silence_duration_ms=200" diff --git a/packages/client/src/scribe/scribe.ts b/packages/client/src/scribe/scribe.ts index 9e492387d..1143f509d 100644 --- a/packages/client/src/scribe/scribe.ts +++ b/packages/client/src/scribe/scribe.ts @@ -81,6 +81,14 @@ interface BaseOptions { * @default false */ noVerbatim?: boolean; + /** + * Whether the request may be logged by ElevenLabs. + * When set to false, zero retention mode is used for the session, which means + * history features are unavailable for it. Zero retention mode may only be + * used by enterprise customers. + * @default true + */ + enableLogging?: boolean; } export interface AudioOptions extends BaseOptions { @@ -201,6 +209,9 @@ export class ScribeRealtime { if (options.noVerbatim !== undefined) { params.append("no_verbatim", options.noVerbatim ? "true" : "false"); } + if (options.enableLogging !== undefined) { + params.append("enable_logging", options.enableLogging ? "true" : "false"); + } const queryString = params.toString(); return queryString ? `${baseUri}?${queryString}` : baseUri; diff --git a/packages/react/src/scribe.test.tsx b/packages/react/src/scribe.test.tsx index 1bcc66949..d1782a8e7 100644 --- a/packages/react/src/scribe.test.tsx +++ b/packages/react/src/scribe.test.tsx @@ -73,4 +73,42 @@ describe("useScribe", () => { }) ); }); + + it("passes enableLogging through to the client", async () => { + const { result } = renderHook(() => useScribe({ enableLogging: false })); + + await act(async () => { + await result.current.connect({ + token: "test-token", + modelId: "scribe_v2_realtime", + audioFormat: AudioFormat.PCM_16000, + sampleRate: 16000, + }); + }); + + expect(Scribe.connect).toHaveBeenCalledWith( + expect.objectContaining({ + enableLogging: false, + }) + ); + }); + + it("lets connect() disable logging for a session enabled at the hook level", async () => { + const { result } = renderHook(() => useScribe({ enableLogging: true })); + + await act(async () => { + await result.current.connect({ + token: "test-token", + modelId: "scribe_v2_realtime", + microphone: {}, + enableLogging: false, + }); + }); + + expect(Scribe.connect).toHaveBeenCalledWith( + expect.objectContaining({ + enableLogging: false, + }) + ); + }); }); diff --git a/packages/react/src/scribe.ts b/packages/react/src/scribe.ts index 382419f19..4688160ee 100644 --- a/packages/react/src/scribe.ts +++ b/packages/react/src/scribe.ts @@ -116,6 +116,14 @@ export interface ScribeHookOptions extends ScribeCallbacks { // Keyterms and verbatim control keyterms?: string[]; noVerbatim?: boolean; + + /** + * Whether the session may be logged by ElevenLabs. Set to `false` to use zero + * retention mode, which makes history features unavailable for the session. + * Zero retention mode may only be used by enterprise customers. + * @default true + */ + enableLogging?: boolean; } export interface UseScribeReturn { @@ -199,6 +207,9 @@ export function useScribe(options: ScribeHookOptions = {}): UseScribeReturn { // Keyterms and verbatim control keyterms: defaultKeyterms, noVerbatim: defaultNoVerbatim, + + // Logging + enableLogging: defaultEnableLogging, } = options; const connectionRef = useRef(null); @@ -258,6 +269,8 @@ export function useScribe(options: ScribeHookOptions = {}): UseScribeReturn { const includeLanguageDetection = runtimeOptions.includeLanguageDetection ?? defaultIncludeLanguageDetection; + const enableLogging = + runtimeOptions.enableLogging ?? defaultEnableLogging; if (microphone) { // Microphone mode @@ -282,6 +295,7 @@ export function useScribe(options: ScribeHookOptions = {}): UseScribeReturn { microphone, includeTimestamps, includeLanguageDetection, + enableLogging, } as MicrophoneOptions); } else if (audioFormat && sampleRate) { // Manual audio mode @@ -305,6 +319,7 @@ export function useScribe(options: ScribeHookOptions = {}): UseScribeReturn { noVerbatim: runtimeOptions.noVerbatim ?? defaultNoVerbatim, includeTimestamps, includeLanguageDetection, + enableLogging, audioFormat, sampleRate, } as AudioOptions); @@ -492,6 +507,7 @@ export function useScribe(options: ScribeHookOptions = {}): UseScribeReturn { defaultIncludeLanguageDetection, defaultKeyterms, defaultNoVerbatim, + defaultEnableLogging, onSessionStarted, onPartialTranscript, onCommittedTranscript, diff --git a/packages/types/generated/types/asyncapi-types.ts b/packages/types/generated/types/asyncapi-types.ts index 0b530f8de..064629e92 100644 --- a/packages/types/generated/types/asyncapi-types.ts +++ b/packages/types/generated/types/asyncapi-types.ts @@ -776,7 +776,7 @@ export interface Config { min_speech_duration_ms?: number; min_silence_duration_ms?: number; model_id?: string; - disable_logging?: boolean; + enable_logging?: boolean; keyterms?: string[]; no_verbatim?: boolean; } diff --git a/packages/types/schemas/scribe.asyncapi.yaml b/packages/types/schemas/scribe.asyncapi.yaml index dc87afd01..bc370a060 100644 --- a/packages/types/schemas/scribe.asyncapi.yaml +++ b/packages/types/schemas/scribe.asyncapi.yaml @@ -347,9 +347,9 @@ components: model_id: type: string description: ID of the model to use for transcription - disable_logging: + enable_logging: type: boolean - description: Whether to disable logging + description: Whether the session may be logged. When false, zero retention mode is used and history features are unavailable for the session. keyterms: type: array items: