@tanstack/ai-elevenlabs 0.4.1 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,7 +27,7 @@ export interface ElevenLabsVoiceSettings {
27
27
  export interface ElevenLabsSpeechProviderOptions {
28
28
  /** ElevenLabs voice ID to synthesize. Required if `generateSpeech().voice` is not set. */
29
29
  voiceId?: string;
30
- /** Output audio format encoded as `codec_samplerate[_bitrate]`. Defaults to `mp3_44100_128`. */
30
+ /** Output audio format encoded as `codec_samplerate[_bitrate]`. Overrides `format`, including WAV wrapping. Defaults to `mp3_44100_128`. */
31
31
  outputFormat?: ElevenLabsOutputFormat;
32
32
  /** Voice-settings overrides for this request only. */
33
33
  voiceSettings?: ElevenLabsVoiceSettings;
@@ -33,6 +33,7 @@ var ElevenLabsSpeechAdapter = class extends BaseTTSAdapter {
33
33
  if (!voiceId) throw new Error("ElevenLabs TTS requires a voice. Pass `voice` on generateSpeech() or `voiceId` in modelOptions.");
34
34
  const { outputFormat, voiceSettings, languageCode, seed, previousText, nextText, previousRequestIds, nextRequestIds, applyTextNormalization, applyLanguageTextNormalization, optimizeStreamingLatency, enableLogging } = options.modelOptions ?? {};
35
35
  const effectiveOutputFormat = outputFormat ?? inferOutputFormatFromResponseFormat(options.format);
36
+ const wrapAsWav = outputFormat == null && options.format === "wav";
36
37
  const stream = await this.client.textToSpeech.convert(voiceId, {
37
38
  text: options.text,
38
39
  modelId: this.model,
@@ -50,8 +51,11 @@ var ElevenLabsSpeechAdapter = class extends BaseTTSAdapter {
50
51
  ...enableLogging != null ? { enableLogging } : {}
51
52
  });
52
53
  const buffer = await readStreamToArrayBuffer(stream);
53
- const base64 = arrayBufferToBase64(buffer);
54
- const { format, contentType } = parseOutputFormat(effectiveOutputFormat);
54
+ const base64 = arrayBufferToBase64(wrapAsWav ? wrapPcmAsWav(buffer) : buffer);
55
+ const { format, contentType } = wrapAsWav ? {
56
+ format: "wav",
57
+ contentType: "audio/wav"
58
+ } : parseOutputFormat(effectiveOutputFormat);
55
59
  return {
56
60
  id: generateId(this.name),
57
61
  model: this.model,
@@ -88,12 +92,34 @@ function mapVoiceSettings(settings, speedOverride) {
88
92
  function inferOutputFormatFromResponseFormat(format) {
89
93
  switch (format) {
90
94
  case "mp3": return "mp3_44100_128";
91
- case "pcm": return "pcm_44100";
95
+ case "pcm":
96
+ case "wav": return "pcm_44100";
92
97
  case "opus": return "opus_48000_128";
93
98
  case void 0: return;
94
- default: return "mp3_44100_128";
99
+ default: throw new Error(`ElevenLabs TTS does not support format '${format}'. Use mp3, pcm, opus, or wav.`);
95
100
  }
96
101
  }
102
+ /** Wrap ElevenLabs pcm_44100 (16-bit little-endian mono) in a RIFF/WAV container. */
103
+ function wrapPcmAsWav(pcm) {
104
+ const wav = new ArrayBuffer(44 + pcm.byteLength);
105
+ const bytes = new Uint8Array(wav);
106
+ const header = new DataView(wav);
107
+ const encoder = new TextEncoder();
108
+ bytes.set(encoder.encode("RIFF"), 0);
109
+ header.setUint32(4, 36 + pcm.byteLength, true);
110
+ bytes.set(encoder.encode("WAVEfmt "), 8);
111
+ header.setUint32(16, 16, true);
112
+ header.setUint16(20, 1, true);
113
+ header.setUint16(22, 1, true);
114
+ header.setUint32(24, 44100, true);
115
+ header.setUint32(28, 88200, true);
116
+ header.setUint16(32, 2, true);
117
+ header.setUint16(34, 16, true);
118
+ bytes.set(encoder.encode("data"), 36);
119
+ header.setUint32(40, pcm.byteLength, true);
120
+ bytes.set(new Uint8Array(pcm), 44);
121
+ return wav;
122
+ }
97
123
  /**
98
124
  * Create an ElevenLabs speech adapter using `ELEVENLABS_API_KEY` from env.
99
125
  */
@@ -1 +1 @@
1
- {"version":3,"file":"speech.js","names":[],"sources":["../../../src/adapters/speech.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport {\n arrayBufferToBase64,\n createElevenLabsClient,\n generateId,\n parseOutputFormat,\n readStreamToArrayBuffer,\n} from '../utils/client'\nimport type { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { ElevenLabsClientConfig } from '../utils/client'\nimport type { ElevenLabsOutputFormat, ElevenLabsTTSModel } from '../model-meta'\n\n/**\n * ElevenLabs voice settings overrides. All fields are optional — omitted\n * values fall back to the voice's stored defaults.\n * @see https://elevenlabs.io/docs/api-reference/text-to-speech/convert\n */\nexport interface ElevenLabsVoiceSettings {\n /** Voice stability, 0..1. Default 0.5. */\n stability?: number\n /** Similarity boost, 0..1. Default 0.75. */\n similarityBoost?: number\n /** Style exaggeration, 0..1. Default 0. */\n style?: number\n /** Playback speed. Default 1.0. */\n speed?: number\n /** Clarity/presence boost. Default true. */\n useSpeakerBoost?: boolean\n}\n\n/**\n * Provider-specific TTS options. `voice` on `generateSpeech()` takes priority\n * over `voiceId` here, but we expose the same field for callers that prefer\n * to keep voice configuration inside the adapter config.\n */\nexport interface ElevenLabsSpeechProviderOptions {\n /** ElevenLabs voice ID to synthesize. Required if `generateSpeech().voice` is not set. */\n voiceId?: string\n /** Output audio format encoded as `codec_samplerate[_bitrate]`. Defaults to `mp3_44100_128`. */\n outputFormat?: ElevenLabsOutputFormat\n /** Voice-settings overrides for this request only. */\n voiceSettings?: ElevenLabsVoiceSettings\n /** ISO-639-1 language code to enforce (e.g. `'en'`, `'ja'`). */\n languageCode?: string\n /** Deterministic sampling seed, 0..4294967295. */\n seed?: number\n /** Previous text for stitching adjacent clips. */\n previousText?: string\n /** Next text for stitching adjacent clips. */\n nextText?: string\n /** Previous request IDs for stitching (max 3). */\n previousRequestIds?: Array<string>\n /** Next request IDs for stitching (max 3). */\n nextRequestIds?: Array<string>\n /** Text normalization toggle. Default `'auto'`. */\n applyTextNormalization?: 'auto' | 'on' | 'off'\n /** Language-specific text normalization (currently Japanese only, adds latency). */\n applyLanguageTextNormalization?: boolean\n /** Latency optimization level, 0..4. */\n optimizeStreamingLatency?: number\n /** Enable logging. Set false for zero-retention mode (enterprise only). */\n enableLogging?: boolean\n}\n\n/**\n * ElevenLabs text-to-speech adapter built on the official\n * `@elevenlabs/elevenlabs-js` SDK.\n *\n * @example\n * ```ts\n * const adapter = elevenlabsSpeech('eleven_multilingual_v2')\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello, world!',\n * voice: '21m00Tcm4TlvDq8ikWAM',\n * })\n * ```\n */\nexport class ElevenLabsSpeechAdapter<\n TModel extends ElevenLabsTTSModel,\n> extends BaseTTSAdapter<TModel, ElevenLabsSpeechProviderOptions> {\n readonly name = 'elevenlabs' as const\n\n private readonly client: ElevenLabsClient\n\n constructor(model: TModel, config?: ElevenLabsClientConfig) {\n super(model, config ?? {})\n this.client = createElevenLabsClient(config)\n }\n\n async generateSpeech(\n options: TTSOptions<ElevenLabsSpeechProviderOptions>,\n ): Promise<TTSResult> {\n const { logger } = options\n logger.request(\n `activity=generateSpeech provider=elevenlabs model=${this.model}`,\n { provider: 'elevenlabs', model: this.model },\n )\n try {\n const voiceId = options.voice ?? options.modelOptions?.voiceId\n if (!voiceId) {\n throw new Error(\n 'ElevenLabs TTS requires a voice. Pass `voice` on generateSpeech() or `voiceId` in modelOptions.',\n )\n }\n const {\n outputFormat,\n voiceSettings,\n languageCode,\n seed,\n previousText,\n nextText,\n previousRequestIds,\n nextRequestIds,\n applyTextNormalization,\n applyLanguageTextNormalization,\n optimizeStreamingLatency,\n enableLogging,\n } = options.modelOptions ?? {}\n const effectiveOutputFormat =\n outputFormat ?? inferOutputFormatFromResponseFormat(options.format)\n\n const stream = await this.client.textToSpeech.convert(voiceId, {\n text: options.text,\n modelId: this.model,\n ...(effectiveOutputFormat\n ? { outputFormat: effectiveOutputFormat }\n : {}),\n ...(voiceSettings\n ? { voiceSettings: mapVoiceSettings(voiceSettings, options.speed) }\n : options.speed != null\n ? { voiceSettings: { speed: options.speed } }\n : {}),\n ...(languageCode ? { languageCode } : {}),\n ...(seed != null ? { seed } : {}),\n ...(previousText ? { previousText } : {}),\n ...(nextText ? { nextText } : {}),\n ...(previousRequestIds ? { previousRequestIds } : {}),\n ...(nextRequestIds ? { nextRequestIds } : {}),\n ...(applyTextNormalization ? { applyTextNormalization } : {}),\n ...(applyLanguageTextNormalization != null\n ? { applyLanguageTextNormalization }\n : {}),\n ...(optimizeStreamingLatency != null\n ? { optimizeStreamingLatency }\n : {}),\n ...(enableLogging != null ? { enableLogging } : {}),\n })\n\n const buffer = await readStreamToArrayBuffer(stream)\n const base64 = arrayBufferToBase64(buffer)\n const { format, contentType } = parseOutputFormat(effectiveOutputFormat)\n\n return {\n id: generateId(this.name),\n model: this.model,\n audio: base64,\n format,\n contentType,\n }\n } catch (error) {\n logger.errors('elevenlabs.generateSpeech fatal', {\n error,\n source: 'elevenlabs.generateSpeech',\n })\n throw error\n }\n }\n\n protected override generateId(): string {\n return generateId(this.name)\n }\n}\n\nfunction mapVoiceSettings(\n settings: ElevenLabsVoiceSettings,\n speedOverride: number | undefined,\n): Record<string, unknown> {\n return {\n ...(settings.stability != null ? { stability: settings.stability } : {}),\n ...(settings.similarityBoost != null\n ? { similarityBoost: settings.similarityBoost }\n : {}),\n ...(settings.style != null ? { style: settings.style } : {}),\n ...(speedOverride != null\n ? { speed: speedOverride }\n : settings.speed != null\n ? { speed: settings.speed }\n : {}),\n ...(settings.useSpeakerBoost != null\n ? { useSpeakerBoost: settings.useSpeakerBoost }\n : {}),\n }\n}\n\n/**\n * Map the standard TTSOptions `format` (mp3/opus/aac/flac/wav/pcm) to a\n * reasonable ElevenLabs `outputFormat` so callers don't need to know the\n * full codec/samplerate string for the common case.\n */\nfunction inferOutputFormatFromResponseFormat(\n format: TTSOptions['format'] | undefined,\n): ElevenLabsOutputFormat | undefined {\n switch (format) {\n case 'mp3':\n return 'mp3_44100_128'\n case 'pcm':\n return 'pcm_44100'\n case 'opus':\n return 'opus_48000_128'\n case undefined:\n return undefined\n case 'aac':\n case 'flac':\n case 'wav':\n default:\n // `aac` / `flac` / `wav` are not native ElevenLabs formats —\n // fall back to mp3 rather than blowing up mid-request.\n return 'mp3_44100_128'\n }\n}\n\n/**\n * Create an ElevenLabs speech adapter using `ELEVENLABS_API_KEY` from env.\n */\nexport function elevenlabsSpeech<TModel extends ElevenLabsTTSModel>(\n model: TModel,\n config?: ElevenLabsClientConfig,\n): ElevenLabsSpeechAdapter<TModel> {\n return new ElevenLabsSpeechAdapter(model, config)\n}\n\n/**\n * Create an ElevenLabs speech adapter with an explicit API key.\n */\nexport function createElevenLabsSpeech<TModel extends ElevenLabsTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<ElevenLabsClientConfig, 'apiKey'>,\n): ElevenLabsSpeechAdapter<TModel> {\n return new ElevenLabsSpeechAdapter(model, { apiKey, ...config })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"speech.js","names":[],"sources":["../../../src/adapters/speech.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport {\n arrayBufferToBase64,\n createElevenLabsClient,\n generateId,\n parseOutputFormat,\n readStreamToArrayBuffer,\n} from '../utils/client'\nimport type { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { ElevenLabsClientConfig } from '../utils/client'\nimport type { ElevenLabsOutputFormat, ElevenLabsTTSModel } from '../model-meta'\n\n/**\n * ElevenLabs voice settings overrides. All fields are optional — omitted\n * values fall back to the voice's stored defaults.\n * @see https://elevenlabs.io/docs/api-reference/text-to-speech/convert\n */\nexport interface ElevenLabsVoiceSettings {\n /** Voice stability, 0..1. Default 0.5. */\n stability?: number\n /** Similarity boost, 0..1. Default 0.75. */\n similarityBoost?: number\n /** Style exaggeration, 0..1. Default 0. */\n style?: number\n /** Playback speed. Default 1.0. */\n speed?: number\n /** Clarity/presence boost. Default true. */\n useSpeakerBoost?: boolean\n}\n\n/**\n * Provider-specific TTS options. `voice` on `generateSpeech()` takes priority\n * over `voiceId` here, but we expose the same field for callers that prefer\n * to keep voice configuration inside the adapter config.\n */\nexport interface ElevenLabsSpeechProviderOptions {\n /** ElevenLabs voice ID to synthesize. Required if `generateSpeech().voice` is not set. */\n voiceId?: string\n /** Output audio format encoded as `codec_samplerate[_bitrate]`. Overrides `format`, including WAV wrapping. Defaults to `mp3_44100_128`. */\n outputFormat?: ElevenLabsOutputFormat\n /** Voice-settings overrides for this request only. */\n voiceSettings?: ElevenLabsVoiceSettings\n /** ISO-639-1 language code to enforce (e.g. `'en'`, `'ja'`). */\n languageCode?: string\n /** Deterministic sampling seed, 0..4294967295. */\n seed?: number\n /** Previous text for stitching adjacent clips. */\n previousText?: string\n /** Next text for stitching adjacent clips. */\n nextText?: string\n /** Previous request IDs for stitching (max 3). */\n previousRequestIds?: Array<string>\n /** Next request IDs for stitching (max 3). */\n nextRequestIds?: Array<string>\n /** Text normalization toggle. Default `'auto'`. */\n applyTextNormalization?: 'auto' | 'on' | 'off'\n /** Language-specific text normalization (currently Japanese only, adds latency). */\n applyLanguageTextNormalization?: boolean\n /** Latency optimization level, 0..4. */\n optimizeStreamingLatency?: number\n /** Enable logging. Set false for zero-retention mode (enterprise only). */\n enableLogging?: boolean\n}\n\n/**\n * ElevenLabs text-to-speech adapter built on the official\n * `@elevenlabs/elevenlabs-js` SDK.\n *\n * @example\n * ```ts\n * const adapter = elevenlabsSpeech('eleven_multilingual_v2')\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello, world!',\n * voice: '21m00Tcm4TlvDq8ikWAM',\n * })\n * ```\n */\nexport class ElevenLabsSpeechAdapter<\n TModel extends ElevenLabsTTSModel,\n> extends BaseTTSAdapter<TModel, ElevenLabsSpeechProviderOptions> {\n readonly name = 'elevenlabs' as const\n\n private readonly client: ElevenLabsClient\n\n constructor(model: TModel, config?: ElevenLabsClientConfig) {\n super(model, config ?? {})\n this.client = createElevenLabsClient(config)\n }\n\n async generateSpeech(\n options: TTSOptions<ElevenLabsSpeechProviderOptions>,\n ): Promise<TTSResult> {\n const { logger } = options\n logger.request(\n `activity=generateSpeech provider=elevenlabs model=${this.model}`,\n { provider: 'elevenlabs', model: this.model },\n )\n try {\n const voiceId = options.voice ?? options.modelOptions?.voiceId\n if (!voiceId) {\n throw new Error(\n 'ElevenLabs TTS requires a voice. Pass `voice` on generateSpeech() or `voiceId` in modelOptions.',\n )\n }\n const {\n outputFormat,\n voiceSettings,\n languageCode,\n seed,\n previousText,\n nextText,\n previousRequestIds,\n nextRequestIds,\n applyTextNormalization,\n applyLanguageTextNormalization,\n optimizeStreamingLatency,\n enableLogging,\n } = options.modelOptions ?? {}\n const effectiveOutputFormat =\n outputFormat ?? inferOutputFormatFromResponseFormat(options.format)\n const wrapAsWav = outputFormat == null && options.format === 'wav'\n\n const stream = await this.client.textToSpeech.convert(voiceId, {\n text: options.text,\n modelId: this.model,\n ...(effectiveOutputFormat\n ? { outputFormat: effectiveOutputFormat }\n : {}),\n ...(voiceSettings\n ? { voiceSettings: mapVoiceSettings(voiceSettings, options.speed) }\n : options.speed != null\n ? { voiceSettings: { speed: options.speed } }\n : {}),\n ...(languageCode ? { languageCode } : {}),\n ...(seed != null ? { seed } : {}),\n ...(previousText ? { previousText } : {}),\n ...(nextText ? { nextText } : {}),\n ...(previousRequestIds ? { previousRequestIds } : {}),\n ...(nextRequestIds ? { nextRequestIds } : {}),\n ...(applyTextNormalization ? { applyTextNormalization } : {}),\n ...(applyLanguageTextNormalization != null\n ? { applyLanguageTextNormalization }\n : {}),\n ...(optimizeStreamingLatency != null\n ? { optimizeStreamingLatency }\n : {}),\n ...(enableLogging != null ? { enableLogging } : {}),\n })\n\n const buffer = await readStreamToArrayBuffer(stream)\n const base64 = arrayBufferToBase64(\n wrapAsWav ? wrapPcmAsWav(buffer) : buffer,\n )\n const { format, contentType } = wrapAsWav\n ? { format: 'wav', contentType: 'audio/wav' }\n : parseOutputFormat(effectiveOutputFormat)\n\n return {\n id: generateId(this.name),\n model: this.model,\n audio: base64,\n format,\n contentType,\n }\n } catch (error) {\n logger.errors('elevenlabs.generateSpeech fatal', {\n error,\n source: 'elevenlabs.generateSpeech',\n })\n throw error\n }\n }\n\n protected override generateId(): string {\n return generateId(this.name)\n }\n}\n\nfunction mapVoiceSettings(\n settings: ElevenLabsVoiceSettings,\n speedOverride: number | undefined,\n): Record<string, unknown> {\n return {\n ...(settings.stability != null ? { stability: settings.stability } : {}),\n ...(settings.similarityBoost != null\n ? { similarityBoost: settings.similarityBoost }\n : {}),\n ...(settings.style != null ? { style: settings.style } : {}),\n ...(speedOverride != null\n ? { speed: speedOverride }\n : settings.speed != null\n ? { speed: settings.speed }\n : {}),\n ...(settings.useSpeakerBoost != null\n ? { useSpeakerBoost: settings.useSpeakerBoost }\n : {}),\n }\n}\n\n/**\n * Map the standard TTSOptions `format` (mp3/opus/aac/flac/wav/pcm) to a\n * reasonable ElevenLabs `outputFormat` so callers don't need to know the\n * full codec/samplerate string for the common case.\n */\nfunction inferOutputFormatFromResponseFormat(\n format: TTSOptions['format'] | undefined,\n): ElevenLabsOutputFormat | undefined {\n switch (format) {\n case 'mp3':\n return 'mp3_44100_128'\n case 'pcm':\n case 'wav':\n return 'pcm_44100'\n case 'opus':\n return 'opus_48000_128'\n case undefined:\n return undefined\n case 'aac':\n case 'flac':\n default:\n throw new Error(\n `ElevenLabs TTS does not support format '${format}'. Use mp3, pcm, opus, or wav.`,\n )\n }\n}\n\n/** Wrap ElevenLabs pcm_44100 (16-bit little-endian mono) in a RIFF/WAV container. */\nfunction wrapPcmAsWav(pcm: ArrayBuffer): ArrayBuffer {\n const wav = new ArrayBuffer(44 + pcm.byteLength)\n const bytes = new Uint8Array(wav)\n const header = new DataView(wav)\n const encoder = new TextEncoder()\n bytes.set(encoder.encode('RIFF'), 0)\n header.setUint32(4, 36 + pcm.byteLength, true)\n bytes.set(encoder.encode('WAVEfmt '), 8)\n header.setUint32(16, 16, true) // PCM format chunk size\n header.setUint16(20, 1, true) // PCM encoding\n header.setUint16(22, 1, true) // mono\n header.setUint32(24, 44100, true) // sample rate\n header.setUint32(28, 44100 * 2, true) // bytes per second\n header.setUint16(32, 2, true) // bytes per sample\n header.setUint16(34, 16, true) // bits per sample\n bytes.set(encoder.encode('data'), 36)\n header.setUint32(40, pcm.byteLength, true)\n bytes.set(new Uint8Array(pcm), 44)\n return wav\n}\n\n/**\n * Create an ElevenLabs speech adapter using `ELEVENLABS_API_KEY` from env.\n */\nexport function elevenlabsSpeech<TModel extends ElevenLabsTTSModel>(\n model: TModel,\n config?: ElevenLabsClientConfig,\n): ElevenLabsSpeechAdapter<TModel> {\n return new ElevenLabsSpeechAdapter(model, config)\n}\n\n/**\n * Create an ElevenLabs speech adapter with an explicit API key.\n */\nexport function createElevenLabsSpeech<TModel extends ElevenLabsTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<ElevenLabsClientConfig, 'apiKey'>,\n): ElevenLabsSpeechAdapter<TModel> {\n return new ElevenLabsSpeechAdapter(model, { apiKey, ...config })\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-elevenlabs",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "ElevenLabs adapter for TanStack AI realtime voice, text-to-speech, transcription, music, and sound effects.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -57,11 +57,11 @@
57
57
  "@tanstack/ai-utils": "^0.4.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "@tanstack/ai": "^0.53.0"
60
+ "@tanstack/ai": "^0.55.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@vitest/coverage-v8": "4.1.10",
64
- "@tanstack/ai": "0.53.0"
64
+ "@tanstack/ai": "0.55.0"
65
65
  },
66
66
  "scripts": {
67
67
  "build": "vite build",
@@ -37,7 +37,7 @@ export interface ElevenLabsVoiceSettings {
37
37
  export interface ElevenLabsSpeechProviderOptions {
38
38
  /** ElevenLabs voice ID to synthesize. Required if `generateSpeech().voice` is not set. */
39
39
  voiceId?: string
40
- /** Output audio format encoded as `codec_samplerate[_bitrate]`. Defaults to `mp3_44100_128`. */
40
+ /** Output audio format encoded as `codec_samplerate[_bitrate]`. Overrides `format`, including WAV wrapping. Defaults to `mp3_44100_128`. */
41
41
  outputFormat?: ElevenLabsOutputFormat
42
42
  /** Voice-settings overrides for this request only. */
43
43
  voiceSettings?: ElevenLabsVoiceSettings
@@ -120,6 +120,7 @@ export class ElevenLabsSpeechAdapter<
120
120
  } = options.modelOptions ?? {}
121
121
  const effectiveOutputFormat =
122
122
  outputFormat ?? inferOutputFormatFromResponseFormat(options.format)
123
+ const wrapAsWav = outputFormat == null && options.format === 'wav'
123
124
 
124
125
  const stream = await this.client.textToSpeech.convert(voiceId, {
125
126
  text: options.text,
@@ -149,8 +150,12 @@ export class ElevenLabsSpeechAdapter<
149
150
  })
150
151
 
151
152
  const buffer = await readStreamToArrayBuffer(stream)
152
- const base64 = arrayBufferToBase64(buffer)
153
- const { format, contentType } = parseOutputFormat(effectiveOutputFormat)
153
+ const base64 = arrayBufferToBase64(
154
+ wrapAsWav ? wrapPcmAsWav(buffer) : buffer,
155
+ )
156
+ const { format, contentType } = wrapAsWav
157
+ ? { format: 'wav', contentType: 'audio/wav' }
158
+ : parseOutputFormat(effectiveOutputFormat)
154
159
 
155
160
  return {
156
161
  id: generateId(this.name),
@@ -206,6 +211,7 @@ function inferOutputFormatFromResponseFormat(
206
211
  case 'mp3':
207
212
  return 'mp3_44100_128'
208
213
  case 'pcm':
214
+ case 'wav':
209
215
  return 'pcm_44100'
210
216
  case 'opus':
211
217
  return 'opus_48000_128'
@@ -213,14 +219,35 @@ function inferOutputFormatFromResponseFormat(
213
219
  return undefined
214
220
  case 'aac':
215
221
  case 'flac':
216
- case 'wav':
217
222
  default:
218
- // `aac` / `flac` / `wav` are not native ElevenLabs formats —
219
- // fall back to mp3 rather than blowing up mid-request.
220
- return 'mp3_44100_128'
223
+ throw new Error(
224
+ `ElevenLabs TTS does not support format '${format}'. Use mp3, pcm, opus, or wav.`,
225
+ )
221
226
  }
222
227
  }
223
228
 
229
+ /** Wrap ElevenLabs pcm_44100 (16-bit little-endian mono) in a RIFF/WAV container. */
230
+ function wrapPcmAsWav(pcm: ArrayBuffer): ArrayBuffer {
231
+ const wav = new ArrayBuffer(44 + pcm.byteLength)
232
+ const bytes = new Uint8Array(wav)
233
+ const header = new DataView(wav)
234
+ const encoder = new TextEncoder()
235
+ bytes.set(encoder.encode('RIFF'), 0)
236
+ header.setUint32(4, 36 + pcm.byteLength, true)
237
+ bytes.set(encoder.encode('WAVEfmt '), 8)
238
+ header.setUint32(16, 16, true) // PCM format chunk size
239
+ header.setUint16(20, 1, true) // PCM encoding
240
+ header.setUint16(22, 1, true) // mono
241
+ header.setUint32(24, 44100, true) // sample rate
242
+ header.setUint32(28, 44100 * 2, true) // bytes per second
243
+ header.setUint16(32, 2, true) // bytes per sample
244
+ header.setUint16(34, 16, true) // bits per sample
245
+ bytes.set(encoder.encode('data'), 36)
246
+ header.setUint32(40, pcm.byteLength, true)
247
+ bytes.set(new Uint8Array(pcm), 44)
248
+ return wav
249
+ }
250
+
224
251
  /**
225
252
  * Create an ElevenLabs speech adapter using `ELEVENLABS_API_KEY` from env.
226
253
  */