diff --git a/javascript/package.json b/javascript/package.json index c69788308..f2aa3375c 100644 --- a/javascript/package.json +++ b/javascript/package.json @@ -59,7 +59,7 @@ "@opentelemetry/sdk-node": "0.212.0", "ai": "^6.0.0", "chalk": "^5.6.2", - "elevenlabs": "^1.59.0", + "@elevenlabs/elevenlabs-js": "^2.51.0", "ffmpeg-static": "5.3.0", "fft.js": "4.0.4", "langwatch": "0.16.1", diff --git a/javascript/pnpm-lock.yaml b/javascript/pnpm-lock.yaml index 4acfbb8a2..6046c4d51 100644 --- a/javascript/pnpm-lock.yaml +++ b/javascript/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@ai-sdk/openai': specifier: ^3.0.26 version: 3.0.65(zod@3.25.76) + '@elevenlabs/elevenlabs-js': + specifier: ^2.51.0 + version: 2.51.0 '@openai/agents': specifier: ^0.3.3 version: 0.3.9(@cfworker/json-schema@4.1.1)(ws@8.20.1)(zod@3.25.76) @@ -50,9 +53,6 @@ importers: chalk: specifier: ^5.6.2 version: 5.6.2 - elevenlabs: - specifier: ^1.59.0 - version: 1.59.0 ffmpeg-static: specifier: 5.3.0 version: 5.3.0 @@ -590,6 +590,10 @@ packages: '@dimforge/rapier3d-compat@0.12.0': resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@elevenlabs/elevenlabs-js@2.51.0': + resolution: {integrity: sha512-Awa9qT3wgF0ak2b9z9l661ZIF97aPyxZ1B8inTrXk4EZn/l2U2Rc6rUqOh45XE7gyrsFSQx6mWp7VBW2IyHiaA==} + engines: {node: '>=18.0.0'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -2316,10 +2320,6 @@ packages: '@webgpu/types@0.1.70': resolution: {integrity: sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==} - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -2439,9 +2439,6 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -2658,10 +2655,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - command-exists@1.2.9: resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} @@ -2810,10 +2803,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2868,10 +2857,6 @@ packages: electron-to-chromium@1.5.360: resolution: {integrity: sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==} - elevenlabs@1.59.0: - resolution: {integrity: sha512-OVKOd+lxNya8h4Rn5fcjv00Asd+DGWfTT6opGrQ16sTI+1HwdLn/kYtjl8tRMhDXbNmksD/9SBRKjb9neiUuVg==} - deprecated: This package has moved to @elevenlabs/elevenlabs-js - emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -3087,17 +3072,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - eventsource-parser@3.0.8: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} @@ -3210,18 +3187,6 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data-encoder@4.1.0: - resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} - engines: {node: '>= 18'} - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - - formdata-node@6.0.3: - resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==} - engines: {node: '>= 18'} - formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -4189,18 +4154,10 @@ packages: meshoptimizer@0.22.0: resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} @@ -4550,10 +4507,6 @@ packages: resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -4634,10 +4587,6 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - readable-stream@4.7.0: - resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -5188,9 +5137,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - url-join@4.0.1: - resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} - use-stick-to-bottom@1.1.1: resolution: {integrity: sha512-JkDp0b0tSmv7HQOOpL1hT7t7QaoUBXkq045WWWOFDTlLGRzgIIyW7vyzOIJzY7L2XVIG7j1yUxeDj2LHm9Vwng==} peerDependencies: @@ -5787,6 +5733,16 @@ snapshots: '@dimforge/rapier3d-compat@0.12.0': {} + '@elevenlabs/elevenlabs-js@2.51.0': + dependencies: + command-exists: 1.2.9 + node-fetch: 2.7.0 + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -7613,10 +7569,6 @@ snapshots: '@webgpu/types@0.1.70': {} - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -7765,8 +7717,6 @@ snapshots: async-function@1.0.0: {} - asynckit@0.4.0: {} - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -7997,10 +7947,6 @@ snapshots: color-name@1.1.4: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - command-exists@1.2.9: {} commander@10.0.1: {} @@ -8122,8 +8068,6 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - delayed-stream@1.0.0: {} - depd@2.0.0: {} detect-gpu@5.0.70: @@ -8171,20 +8115,6 @@ snapshots: electron-to-chromium@1.5.360: {} - elevenlabs@1.59.0: - dependencies: - command-exists: 1.2.9 - execa: 5.1.1 - form-data: 4.0.5 - form-data-encoder: 4.1.0 - formdata-node: 6.0.3 - node-fetch: 2.7.0 - qs: 6.15.2 - readable-stream: 4.7.0 - url-join: 4.0.1 - transitivePeerDependencies: - - encoding - emittery@0.13.1: {} emoji-regex@8.0.0: {} @@ -8523,12 +8453,8 @@ snapshots: etag@1.8.1: {} - event-target-shim@5.0.1: {} - eventemitter3@4.0.7: {} - events@3.3.0: {} - eventsource-parser@3.0.8: {} eventsource@3.0.7: @@ -8686,18 +8612,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data-encoder@4.1.0: {} - - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.3 - mime-types: 2.1.35 - - formdata-node@6.0.3: {} - formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -9776,14 +9690,8 @@ snapshots: meshoptimizer@0.22.0: {} - mime-db@1.52.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -10106,8 +10014,6 @@ snapshots: react-is-18: react-is@18.3.1 react-is-19: react-is@19.2.6 - process@0.11.10: {} - progress@2.0.3: {} promise-worker-transferable@1.0.4: @@ -10191,14 +10097,6 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - readable-stream@4.7.0: - dependencies: - abort-controller: 3.0.0 - buffer: 6.0.3 - events: 3.3.0 - process: 0.11.10 - string_decoder: 1.3.0 - readdirp@4.1.2: {} reflect.getprototypeof@1.0.10: @@ -10860,8 +10758,6 @@ snapshots: dependencies: punycode: 2.3.1 - url-join@4.0.1: {} - use-stick-to-bottom@1.1.1(react@19.2.6): dependencies: react: 19.2.6 diff --git a/javascript/src/voice/adapters/__tests__/elevenlabs.test.ts b/javascript/src/voice/adapters/__tests__/elevenlabs.test.ts index 5e773ec34..94cd045e0 100644 --- a/javascript/src/voice/adapters/__tests__/elevenlabs.test.ts +++ b/javascript/src/voice/adapters/__tests__/elevenlabs.test.ts @@ -185,6 +185,12 @@ describeFeature( let llmCalls: { current: number }; const sttCalls: { transcribed: AudioChunk[] } = { transcribed: [] }; const ttsCalls: { text: string[] } = { text: [] }; + // Captures the EL SDK `textToSpeech.convert` spy so the scenario can + // assert the request body (modelId/outputFormat), not just the + // recorded text — a wrong-model regression must fail here. + const ttsConvertSpy: { current: ReturnType | null } = { + current: null, + }; Given( "an STTProvider implementation, an LLM identifier, and a TTSProvider identifier from any supported providers", @@ -203,7 +209,8 @@ describeFeature( tts: "elevenlabs/test-voice", ttsOptions: { apiKey: "sk_fake_eleven", - elevenLabsClientFactory: () => makeFakeElevenClient(ttsCalls.text), + elevenLabsClientFactory: () => + makeFakeElevenClient(ttsCalls.text, ttsConvertSpy), }, }); }, @@ -231,6 +238,18 @@ describeFeature( expect(sttCalls.transcribed).toHaveLength(1); expect(llmCalls.current).toBe(1); expect(ttsCalls.text).toEqual(["hello back"]); + // Pin the EL `textToSpeech.convert` request body, not just the + // recorded text: voiceId is positional arg 1, the body is arg 2. + // The concrete modelId/outputFormat catch a wrong-model regression + // that a call-count-only assertion would let through silently. + expect(ttsConvertSpy.current).toHaveBeenCalledWith( + "test-voice", + expect.objectContaining({ + text: "hello back", + modelId: "eleven_v3", + outputFormat: "pcm_24000", + }), + ); expect(out).toBeInstanceOf(AudioChunk); expect(out.data.length).toBeGreaterThan(0); @@ -399,6 +418,12 @@ describeFeature( expect(typeof text).toBe("string"); expect(text).toBe("transcribed text"); expect(fakeClient.speechToText.convert).toHaveBeenCalledTimes(1); + // `speechToText.convert` takes a single body object (arg 1). Pin the + // concrete modelId so a wrong-model regression fails — call-count + // alone would pass with the wrong scribe model. + expect(fakeClient.speechToText.convert).toHaveBeenCalledWith( + expect.objectContaining({ modelId: "scribe_v1" }), + ); }, ); @@ -440,18 +465,26 @@ function silentChunkBytes(seconds: number): Uint8Array { return new Uint8Array(samples * 2); } -function makeFakeElevenClient(recordedText: string[]): never { - const fake = { - textToSpeech: { - convert: vi.fn(async (_voiceId: string, request: { text: string }) => { - recordedText.push(request.text); - // Return an async iterable of one PCM16 chunk. - const buf = Buffer.from(new Uint8Array([0x01, 0x00, 0x02, 0x00])); - return (async function* () { - yield buf; - })(); - }), +function makeFakeElevenClient( + recordedText: string[], + spyRef?: { current: ReturnType | null }, +): never { + // The real adapter calls `textToSpeech.convert(voiceId, { text, modelId, + // outputFormat })`. The spy records BOTH args verbatim, so a body assertion + // on `spyRef.current` sees the actual modelId/outputFormat the adapter sent. + const convert = vi.fn( + async (_voiceId: string, request: { text: string }) => { + recordedText.push(request.text); + // Return an async iterable of one PCM16 chunk. + const buf = Buffer.from(new Uint8Array([0x01, 0x00, 0x02, 0x00])); + return (async function* () { + yield buf; + })(); }, + ); + if (spyRef) spyRef.current = convert; + const fake = { + textToSpeech: { convert }, }; return fake as never; } diff --git a/javascript/src/voice/stt/elevenlabs-stt.ts b/javascript/src/voice/stt/elevenlabs-stt.ts index 5e6836bc3..65426bfac 100644 --- a/javascript/src/voice/stt/elevenlabs-stt.ts +++ b/javascript/src/voice/stt/elevenlabs-stt.ts @@ -2,7 +2,7 @@ * ElevenLabs STT leaf — {@link ElevenLabsSTTProvider} (Scribe). Same * {@link STTProvider} contract as the OpenAI leaf, different backend. * - * Uses the `scribe_v1` model via the `elevenlabs` SDK's + * Uses the `scribe_v1` model via the `@elevenlabs/elevenlabs-js` SDK's * `speechToText.convert`. PCM16/24 kHz audio is wrapped in a minimal WAV * container before posting (EL's endpoint expects a file payload, not raw * PCM). Only `text` crosses the {@link STTProvider} boundary — no @@ -12,7 +12,7 @@ * copy that used to live in `adapters/composable.ts` is gone; composable and * the branded preset import this leaf. */ -import { ElevenLabsClient } from "elevenlabs"; +import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js"; import { AudioChunk } from "../audio-chunk"; import { ELEVENLABS_STT_MODEL } from "../voice-models"; @@ -56,7 +56,7 @@ export class ElevenLabsSTTProvider implements STTProvider { const blob = new Blob([new Uint8Array(wav)], { type: "audio/wav" }); const response = await client.speechToText.convert({ file: blob, - model_id: ELEVENLABS_STT_MODEL, + modelId: ELEVENLABS_STT_MODEL, }); return response.text ?? ""; } diff --git a/javascript/src/voice/tts/elevenlabs-tts.ts b/javascript/src/voice/tts/elevenlabs-tts.ts index 05eb36b89..0683e8e9e 100644 --- a/javascript/src/voice/tts/elevenlabs-tts.ts +++ b/javascript/src/voice/tts/elevenlabs-tts.ts @@ -6,15 +6,15 @@ * TTS registry instead of being buried in `adapters/composable.ts`. The * composable agent consumes this leaf for its EL path (de-dup, Gap #5). * - * Wire: `client.textToSpeech.convert(voiceId, { model_id: eleven_v3, - * output_format: "pcm_24000" })` → raw PCM16/24 kHz mono, matching the + * Wire: `client.textToSpeech.convert(voiceId, { modelId: eleven_v3, + * outputFormat: "pcm_24000" })` → raw PCM16/24 kHz mono, matching the * canonical {@link AudioChunk}. * * Registered under the `elevenlabs` prefix by `tts/index.ts` (side effect). */ import { Buffer } from "node:buffer"; -import { ElevenLabsClient } from "elevenlabs"; +import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js"; import { ELEVENLABS_TTS_MODEL } from "../voice-models"; import type { TTSCallable } from "./tts"; @@ -49,8 +49,8 @@ export async function elevenLabsSynthesizeBytes( const client = factory(apiKey); const stream = await client.textToSpeech.convert(voiceId, { text, - model_id: ELEVENLABS_TTS_MODEL, - output_format: "pcm_24000", + modelId: ELEVENLABS_TTS_MODEL, + outputFormat: "pcm_24000", }); const chunks: Buffer[] = []; for await (const chunk of stream as AsyncIterable) {