@tanstack/ai-elevenlabs 0.4.2 → 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 } =
|
|
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":
|
|
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:
|
|
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.
|
|
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.
|
|
60
|
+
"@tanstack/ai": "^0.55.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@vitest/coverage-v8": "4.1.10",
|
|
64
|
-
"@tanstack/ai": "0.
|
|
64
|
+
"@tanstack/ai": "0.55.0"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"build": "vite build",
|
package/src/adapters/speech.ts
CHANGED
|
@@ -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(
|
|
153
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
*/
|