@tanstack/ai-elevenlabs 0.2.7 → 0.2.9

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.
@@ -74,7 +74,7 @@ export type ElevenLabsAudioProviderOptions = (ElevenLabsMusicProviderOptions & E
74
74
  */
75
75
  export declare class ElevenLabsAudioAdapter<TModel extends ElevenLabsAudioModel> extends BaseAudioAdapter<TModel, ElevenLabsAudioProviderOptions> {
76
76
  readonly name: "elevenlabs";
77
- private client;
77
+ private readonly client;
78
78
  constructor(model: TModel, config?: ElevenLabsClientConfig);
79
79
  generateAudio(options: AudioGenerationOptions<ElevenLabsAudioProviderOptions>): Promise<AudioGenerationResult>;
80
80
  private runMusic;
@@ -2,9 +2,10 @@ import { BaseAudioAdapter } from "@tanstack/ai/adapters";
2
2
  import { createElevenLabsClient, readStreamToArrayBuffer, arrayBufferToBase64, parseOutputFormat, generateId } from "../utils/client.js";
3
3
  import { isElevenLabsMusicModel, isElevenLabsSoundEffectsModel } from "../model-meta.js";
4
4
  class ElevenLabsAudioAdapter extends BaseAudioAdapter {
5
+ name = "elevenlabs";
6
+ client;
5
7
  constructor(model, config) {
6
8
  super(model, config ?? {});
7
- this.name = "elevenlabs";
8
9
  this.client = createElevenLabsClient(config);
9
10
  }
10
11
  async generateAudio(options) {
@@ -1 +1 @@
1
- {"version":3,"file":"audio.js","sources":["../../../src/adapters/audio.ts"],"sourcesContent":["import { BaseAudioAdapter } from '@tanstack/ai/adapters'\nimport {\n arrayBufferToBase64,\n createElevenLabsClient,\n generateId,\n parseOutputFormat,\n readStreamToArrayBuffer,\n} from '../utils/client'\nimport {\n isElevenLabsMusicModel,\n isElevenLabsSoundEffectsModel,\n} from '../model-meta'\nimport type { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'\nimport type {\n AudioGenerationOptions,\n AudioGenerationResult,\n} from '@tanstack/ai'\nimport type { ElevenLabsClientConfig } from '../utils/client'\nimport type {\n ElevenLabsAudioModel,\n ElevenLabsMusicModel,\n ElevenLabsOutputFormat,\n ElevenLabsSoundEffectsModel,\n} from '../model-meta'\n\n/**\n * Structured composition plan for ElevenLabs music generation. Mutually\n * exclusive with a free-form `prompt` on the `generateAudio()` call — when\n * supplied, `prompt` is ignored by ElevenLabs.\n *\n * We mirror the SDK's camelCase naming. Lengths are in milliseconds.\n * @see https://elevenlabs.io/docs/api-reference/music/compose\n */\nexport interface ElevenLabsMusicCompositionPlan {\n /** Positive global style descriptors (mood, instruments, tempo, …). */\n positiveGlobalStyles?: Array<string>\n /** Negative global style descriptors — styles to avoid. */\n negativeGlobalStyles?: Array<string>\n /** Section definitions (verse/chorus/bridge/…) with local style hints. */\n sections?: Array<{\n sectionName: string\n positiveLocalStyles?: Array<string>\n negativeLocalStyles?: Array<string>\n durationMs?: number\n lines?: Array<string>\n }>\n}\n\n/**\n * Provider options common to all ElevenLabs audio endpoints.\n */\ninterface CommonAudioOptions {\n /** Output audio format. Defaults to `mp3_44100_128`. */\n outputFormat?: ElevenLabsOutputFormat\n}\n\n/**\n * Provider options for music generation (`music_v1`).\n */\nexport interface ElevenLabsMusicProviderOptions extends CommonAudioOptions {\n /** Structured composition plan. Mutually exclusive with `prompt`/`duration`. */\n compositionPlan?: ElevenLabsMusicCompositionPlan\n /** Deterministic sampling seed (incompatible with `prompt`). */\n seed?: number\n /** Force the output to be purely instrumental (prompt-mode only). */\n forceInstrumental?: boolean\n /** Strictly respect section durations in `compositionPlan`. */\n respectSectionsDurations?: boolean\n}\n\n/**\n * Provider options for sound-effect generation (`eleven_text_to_sound_v*`).\n */\nexport interface ElevenLabsSoundEffectsProviderOptions extends CommonAudioOptions {\n /** Prompt influence, 0..1. Default 0.3. Higher = more prompt adherence. */\n promptInfluence?: number\n /** Generate a loopable SFX (v2 only). */\n loop?: boolean\n}\n\n/**\n * Union of per-model provider options. We keep both branches on one type so\n * the adapter stays tree-shakeable; callers narrow by model at the factory.\n */\nexport type ElevenLabsAudioProviderOptions =\n | (ElevenLabsMusicProviderOptions & ElevenLabsSoundEffectsProviderOptions)\n | ElevenLabsMusicProviderOptions\n | ElevenLabsSoundEffectsProviderOptions\n\n/**\n * ElevenLabs audio generation adapter. Dispatches to music or SFX endpoints\n * based on the model id. Music → `client.music.compose`, SFX →\n * `client.textToSoundEffects.convert`.\n *\n * @example\n * ```ts\n * const music = elevenlabsAudio('music_v1')\n * await generateAudio({ adapter: music, prompt: 'lo-fi beat', duration: 15 })\n *\n * const sfx = elevenlabsAudio('eleven_text_to_sound_v2')\n * await generateAudio({ adapter: sfx, prompt: 'glass shattering', duration: 3 })\n * ```\n */\nexport class ElevenLabsAudioAdapter<\n TModel extends ElevenLabsAudioModel,\n> extends BaseAudioAdapter<TModel, ElevenLabsAudioProviderOptions> {\n readonly name = 'elevenlabs' as const\n\n private client: ElevenLabsClient\n\n constructor(model: TModel, config?: ElevenLabsClientConfig) {\n super(model, config ?? {})\n this.client = createElevenLabsClient(config)\n }\n\n async generateAudio(\n options: AudioGenerationOptions<ElevenLabsAudioProviderOptions>,\n ): Promise<AudioGenerationResult> {\n const { logger } = options\n logger.request(\n `activity=generateAudio provider=elevenlabs model=${this.model}`,\n { provider: 'elevenlabs', model: this.model },\n )\n try {\n if (isElevenLabsMusicModel(this.model)) {\n return await this.runMusic(options)\n }\n if (isElevenLabsSoundEffectsModel(this.model)) {\n return await this.runSoundEffects(options)\n }\n throw new Error(\n `Unsupported ElevenLabs audio model \"${this.model}\". Expected one of: music_v1, eleven_text_to_sound_v2, eleven_text_to_sound_v1.`,\n )\n } catch (error) {\n logger.errors('elevenlabs.generateAudio fatal', {\n error,\n source: 'elevenlabs.generateAudio',\n })\n throw error\n }\n }\n\n private async runMusic(\n options: AudioGenerationOptions<ElevenLabsAudioProviderOptions>,\n ): Promise<AudioGenerationResult> {\n // Gated by isElevenLabsMusicModel() in generateAudio().\n const modelId = this.model as ElevenLabsMusicModel\n const music = (options.modelOptions ?? {}) as ElevenLabsMusicProviderOptions\n const outputFormat = music.outputFormat\n\n const stream = await this.client.music.compose({\n modelId,\n ...(options.prompt && !music.compositionPlan\n ? { prompt: options.prompt }\n : {}),\n ...(music.compositionPlan\n ? { compositionPlan: toMusicPrompt(music.compositionPlan) }\n : {}),\n ...(options.duration != null && !music.compositionPlan\n ? { musicLengthMs: Math.round(options.duration * 1000) }\n : {}),\n ...(outputFormat ? { outputFormat } : {}),\n ...(music.seed != null ? { seed: music.seed } : {}),\n ...(music.forceInstrumental != null\n ? { forceInstrumental: music.forceInstrumental }\n : {}),\n ...(music.respectSectionsDurations != null\n ? { respectSectionsDurations: music.respectSectionsDurations }\n : {}),\n })\n\n return this.finalize(stream, outputFormat, options.duration)\n }\n\n private async runSoundEffects(\n options: AudioGenerationOptions<ElevenLabsAudioProviderOptions>,\n ): Promise<AudioGenerationResult> {\n // Gated by isElevenLabsSoundEffectsModel() in generateAudio().\n const modelId = this.model as ElevenLabsSoundEffectsModel\n const sfx = (options.modelOptions ??\n {}) as ElevenLabsSoundEffectsProviderOptions\n const outputFormat = sfx.outputFormat\n\n const stream = await this.client.textToSoundEffects.convert({\n text: options.prompt,\n modelId,\n ...(options.duration != null\n ? { durationSeconds: options.duration }\n : {}),\n ...(outputFormat ? { outputFormat } : {}),\n ...(sfx.promptInfluence != null\n ? { promptInfluence: sfx.promptInfluence }\n : {}),\n ...(sfx.loop != null ? { loop: sfx.loop } : {}),\n })\n\n return this.finalize(stream, outputFormat, options.duration)\n }\n\n private async finalize(\n stream: ReadableStream<Uint8Array>,\n outputFormat: ElevenLabsOutputFormat | undefined,\n duration: number | undefined,\n ): Promise<AudioGenerationResult> {\n const buffer = await readStreamToArrayBuffer(stream)\n const base64 = arrayBufferToBase64(buffer)\n const { contentType } = parseOutputFormat(outputFormat)\n return {\n id: generateId(this.name),\n model: this.model,\n audio: {\n b64Json: base64,\n contentType,\n ...(duration != null ? { duration } : {}),\n },\n }\n }\n\n protected override generateId(): string {\n return generateId(this.name)\n }\n}\n\nfunction toMusicPrompt(plan: ElevenLabsMusicCompositionPlan) {\n return {\n positiveGlobalStyles: plan.positiveGlobalStyles ?? [],\n negativeGlobalStyles: plan.negativeGlobalStyles ?? [],\n sections: (plan.sections ?? []).map((section) => ({\n sectionName: section.sectionName,\n positiveLocalStyles: section.positiveLocalStyles ?? [],\n negativeLocalStyles: section.negativeLocalStyles ?? [],\n durationMs: section.durationMs ?? 10000,\n lines: section.lines ?? [],\n })),\n }\n}\n\n/**\n * Create an ElevenLabs audio adapter using `ELEVENLABS_API_KEY` from env.\n */\nexport function elevenlabsAudio<TModel extends ElevenLabsAudioModel>(\n model: TModel,\n config?: ElevenLabsClientConfig,\n): ElevenLabsAudioAdapter<TModel> {\n return new ElevenLabsAudioAdapter(model, config)\n}\n\n/**\n * Create an ElevenLabs audio adapter with an explicit API key.\n */\nexport function createElevenLabsAudio<TModel extends ElevenLabsAudioModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<ElevenLabsClientConfig, 'apiKey'>,\n): ElevenLabsAudioAdapter<TModel> {\n return new ElevenLabsAudioAdapter(model, { apiKey, ...config })\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"audio.js","sources":["../../../src/adapters/audio.ts"],"sourcesContent":["import { BaseAudioAdapter } from '@tanstack/ai/adapters'\nimport {\n arrayBufferToBase64,\n createElevenLabsClient,\n generateId,\n parseOutputFormat,\n readStreamToArrayBuffer,\n} from '../utils/client'\nimport {\n isElevenLabsMusicModel,\n isElevenLabsSoundEffectsModel,\n} from '../model-meta'\nimport type { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'\nimport type {\n AudioGenerationOptions,\n AudioGenerationResult,\n} from '@tanstack/ai'\nimport type { ElevenLabsClientConfig } from '../utils/client'\nimport type {\n ElevenLabsAudioModel,\n ElevenLabsMusicModel,\n ElevenLabsOutputFormat,\n ElevenLabsSoundEffectsModel,\n} from '../model-meta'\n\n/**\n * Structured composition plan for ElevenLabs music generation. Mutually\n * exclusive with a free-form `prompt` on the `generateAudio()` call — when\n * supplied, `prompt` is ignored by ElevenLabs.\n *\n * We mirror the SDK's camelCase naming. Lengths are in milliseconds.\n * @see https://elevenlabs.io/docs/api-reference/music/compose\n */\nexport interface ElevenLabsMusicCompositionPlan {\n /** Positive global style descriptors (mood, instruments, tempo, …). */\n positiveGlobalStyles?: Array<string>\n /** Negative global style descriptors — styles to avoid. */\n negativeGlobalStyles?: Array<string>\n /** Section definitions (verse/chorus/bridge/…) with local style hints. */\n sections?: Array<{\n sectionName: string\n positiveLocalStyles?: Array<string>\n negativeLocalStyles?: Array<string>\n durationMs?: number\n lines?: Array<string>\n }>\n}\n\n/**\n * Provider options common to all ElevenLabs audio endpoints.\n */\ninterface CommonAudioOptions {\n /** Output audio format. Defaults to `mp3_44100_128`. */\n outputFormat?: ElevenLabsOutputFormat\n}\n\n/**\n * Provider options for music generation (`music_v1`).\n */\nexport interface ElevenLabsMusicProviderOptions extends CommonAudioOptions {\n /** Structured composition plan. Mutually exclusive with `prompt`/`duration`. */\n compositionPlan?: ElevenLabsMusicCompositionPlan\n /** Deterministic sampling seed (incompatible with `prompt`). */\n seed?: number\n /** Force the output to be purely instrumental (prompt-mode only). */\n forceInstrumental?: boolean\n /** Strictly respect section durations in `compositionPlan`. */\n respectSectionsDurations?: boolean\n}\n\n/**\n * Provider options for sound-effect generation (`eleven_text_to_sound_v*`).\n */\nexport interface ElevenLabsSoundEffectsProviderOptions extends CommonAudioOptions {\n /** Prompt influence, 0..1. Default 0.3. Higher = more prompt adherence. */\n promptInfluence?: number\n /** Generate a loopable SFX (v2 only). */\n loop?: boolean\n}\n\n/**\n * Union of per-model provider options. We keep both branches on one type so\n * the adapter stays tree-shakeable; callers narrow by model at the factory.\n */\nexport type ElevenLabsAudioProviderOptions =\n | (ElevenLabsMusicProviderOptions & ElevenLabsSoundEffectsProviderOptions)\n | ElevenLabsMusicProviderOptions\n | ElevenLabsSoundEffectsProviderOptions\n\n/**\n * ElevenLabs audio generation adapter. Dispatches to music or SFX endpoints\n * based on the model id. Music → `client.music.compose`, SFX →\n * `client.textToSoundEffects.convert`.\n *\n * @example\n * ```ts\n * const music = elevenlabsAudio('music_v1')\n * await generateAudio({ adapter: music, prompt: 'lo-fi beat', duration: 15 })\n *\n * const sfx = elevenlabsAudio('eleven_text_to_sound_v2')\n * await generateAudio({ adapter: sfx, prompt: 'glass shattering', duration: 3 })\n * ```\n */\nexport class ElevenLabsAudioAdapter<\n TModel extends ElevenLabsAudioModel,\n> extends BaseAudioAdapter<TModel, ElevenLabsAudioProviderOptions> {\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 generateAudio(\n options: AudioGenerationOptions<ElevenLabsAudioProviderOptions>,\n ): Promise<AudioGenerationResult> {\n const { logger } = options\n logger.request(\n `activity=generateAudio provider=elevenlabs model=${this.model}`,\n { provider: 'elevenlabs', model: this.model },\n )\n try {\n if (isElevenLabsMusicModel(this.model)) {\n return await this.runMusic(options)\n }\n if (isElevenLabsSoundEffectsModel(this.model)) {\n return await this.runSoundEffects(options)\n }\n throw new Error(\n `Unsupported ElevenLabs audio model \"${this.model}\". Expected one of: music_v1, eleven_text_to_sound_v2, eleven_text_to_sound_v1.`,\n )\n } catch (error) {\n logger.errors('elevenlabs.generateAudio fatal', {\n error,\n source: 'elevenlabs.generateAudio',\n })\n throw error\n }\n }\n\n private async runMusic(\n options: AudioGenerationOptions<ElevenLabsAudioProviderOptions>,\n ): Promise<AudioGenerationResult> {\n // Gated by isElevenLabsMusicModel() in generateAudio().\n const modelId = this.model as ElevenLabsMusicModel\n const music = (options.modelOptions ?? {}) as ElevenLabsMusicProviderOptions\n const outputFormat = music.outputFormat\n\n const stream = await this.client.music.compose({\n modelId,\n ...(options.prompt && !music.compositionPlan\n ? { prompt: options.prompt }\n : {}),\n ...(music.compositionPlan\n ? { compositionPlan: toMusicPrompt(music.compositionPlan) }\n : {}),\n ...(options.duration != null && !music.compositionPlan\n ? { musicLengthMs: Math.round(options.duration * 1000) }\n : {}),\n ...(outputFormat ? { outputFormat } : {}),\n ...(music.seed != null ? { seed: music.seed } : {}),\n ...(music.forceInstrumental != null\n ? { forceInstrumental: music.forceInstrumental }\n : {}),\n ...(music.respectSectionsDurations != null\n ? { respectSectionsDurations: music.respectSectionsDurations }\n : {}),\n })\n\n return this.finalize(stream, outputFormat, options.duration)\n }\n\n private async runSoundEffects(\n options: AudioGenerationOptions<ElevenLabsAudioProviderOptions>,\n ): Promise<AudioGenerationResult> {\n // Gated by isElevenLabsSoundEffectsModel() in generateAudio().\n const modelId = this.model as ElevenLabsSoundEffectsModel\n const sfx = (options.modelOptions ??\n {}) as ElevenLabsSoundEffectsProviderOptions\n const outputFormat = sfx.outputFormat\n\n const stream = await this.client.textToSoundEffects.convert({\n text: options.prompt,\n modelId,\n ...(options.duration != null\n ? { durationSeconds: options.duration }\n : {}),\n ...(outputFormat ? { outputFormat } : {}),\n ...(sfx.promptInfluence != null\n ? { promptInfluence: sfx.promptInfluence }\n : {}),\n ...(sfx.loop != null ? { loop: sfx.loop } : {}),\n })\n\n return this.finalize(stream, outputFormat, options.duration)\n }\n\n private async finalize(\n stream: ReadableStream<Uint8Array>,\n outputFormat: ElevenLabsOutputFormat | undefined,\n duration: number | undefined,\n ): Promise<AudioGenerationResult> {\n const buffer = await readStreamToArrayBuffer(stream)\n const base64 = arrayBufferToBase64(buffer)\n const { contentType } = parseOutputFormat(outputFormat)\n return {\n id: generateId(this.name),\n model: this.model,\n audio: {\n b64Json: base64,\n contentType,\n ...(duration != null ? { duration } : {}),\n },\n }\n }\n\n protected override generateId(): string {\n return generateId(this.name)\n }\n}\n\nfunction toMusicPrompt(plan: ElevenLabsMusicCompositionPlan) {\n return {\n positiveGlobalStyles: plan.positiveGlobalStyles ?? [],\n negativeGlobalStyles: plan.negativeGlobalStyles ?? [],\n sections: (plan.sections ?? []).map((section) => ({\n sectionName: section.sectionName,\n positiveLocalStyles: section.positiveLocalStyles ?? [],\n negativeLocalStyles: section.negativeLocalStyles ?? [],\n durationMs: section.durationMs ?? 10000,\n lines: section.lines ?? [],\n })),\n }\n}\n\n/**\n * Create an ElevenLabs audio adapter using `ELEVENLABS_API_KEY` from env.\n */\nexport function elevenlabsAudio<TModel extends ElevenLabsAudioModel>(\n model: TModel,\n config?: ElevenLabsClientConfig,\n): ElevenLabsAudioAdapter<TModel> {\n return new ElevenLabsAudioAdapter(model, config)\n}\n\n/**\n * Create an ElevenLabs audio adapter with an explicit API key.\n */\nexport function createElevenLabsAudio<TModel extends ElevenLabsAudioModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<ElevenLabsClientConfig, 'apiKey'>,\n): ElevenLabsAudioAdapter<TModel> {\n return new ElevenLabsAudioAdapter(model, { apiKey, ...config })\n}\n"],"names":[],"mappings":"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"}
@@ -68,7 +68,7 @@ export interface ElevenLabsSpeechProviderOptions {
68
68
  */
69
69
  export declare class ElevenLabsSpeechAdapter<TModel extends ElevenLabsTTSModel> extends BaseTTSAdapter<TModel, ElevenLabsSpeechProviderOptions> {
70
70
  readonly name: "elevenlabs";
71
- private client;
71
+ private readonly client;
72
72
  constructor(model: TModel, config?: ElevenLabsClientConfig);
73
73
  generateSpeech(options: TTSOptions<ElevenLabsSpeechProviderOptions>): Promise<TTSResult>;
74
74
  protected generateId(): string;
@@ -1,9 +1,10 @@
1
1
  import { BaseTTSAdapter } from "@tanstack/ai/adapters";
2
2
  import { createElevenLabsClient, readStreamToArrayBuffer, arrayBufferToBase64, parseOutputFormat, generateId } from "../utils/client.js";
3
3
  class ElevenLabsSpeechAdapter extends BaseTTSAdapter {
4
+ name = "elevenlabs";
5
+ client;
4
6
  constructor(model, config) {
5
7
  super(model, config ?? {});
6
- this.name = "elevenlabs";
7
8
  this.client = createElevenLabsClient(config);
8
9
  }
9
10
  async generateSpeech(options) {
@@ -91,6 +92,9 @@ function inferOutputFormatFromResponseFormat(format) {
91
92
  return "opus_48000_128";
92
93
  case void 0:
93
94
  return void 0;
95
+ case "aac":
96
+ case "flac":
97
+ case "wav":
94
98
  default:
95
99
  return "mp3_44100_128";
96
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"speech.js","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 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 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"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"speech.js","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"],"names":[],"mappings":"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"}
@@ -63,7 +63,7 @@ export interface ElevenLabsTranscriptionProviderOptions {
63
63
  */
64
64
  export declare class ElevenLabsTranscriptionAdapter<TModel extends ElevenLabsTranscriptionModel> extends BaseTranscriptionAdapter<TModel, ElevenLabsTranscriptionProviderOptions> {
65
65
  readonly name: "elevenlabs";
66
- private client;
66
+ private readonly client;
67
67
  constructor(model: TModel, config?: ElevenLabsClientConfig);
68
68
  transcribe(options: TranscriptionOptions<ElevenLabsTranscriptionProviderOptions>): Promise<TranscriptionResult>;
69
69
  private transformResponse;
@@ -1,9 +1,10 @@
1
1
  import { BaseTranscriptionAdapter } from "@tanstack/ai/adapters";
2
2
  import { createElevenLabsClient, generateId, dataUrlToBlob } from "../utils/client.js";
3
3
  class ElevenLabsTranscriptionAdapter extends BaseTranscriptionAdapter {
4
+ name = "elevenlabs";
5
+ client;
4
6
  constructor(model, config) {
5
7
  super(model, config ?? {});
6
- this.name = "elevenlabs";
7
8
  this.client = createElevenLabsClient(config);
8
9
  }
9
10
  async transcribe(options) {
@@ -1 +1 @@
1
- {"version":3,"file":"transcription.js","sources":["../../../src/adapters/transcription.ts"],"sourcesContent":["import { BaseTranscriptionAdapter } from '@tanstack/ai/adapters'\nimport {\n createElevenLabsClient,\n dataUrlToBlob,\n generateId,\n} from '../utils/client'\nimport type { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'\nimport type {\n TranscriptionOptions,\n TranscriptionResult,\n TranscriptionSegment,\n TranscriptionWord,\n} from '@tanstack/ai'\nimport type { ElevenLabsClientConfig } from '../utils/client'\nimport type { ElevenLabsTranscriptionModel } from '../model-meta'\n\n/**\n * Provider-specific options for ElevenLabs Scribe transcription. Fields map\n * 1:1 onto the SDK's `BodySpeechToTextV1SpeechToTextPost` — mirroring the\n * names so documentation stays useful.\n * @see https://elevenlabs.io/docs/api-reference/speech-to-text/convert\n */\nexport interface ElevenLabsTranscriptionProviderOptions {\n /** Annotate non-speech events like (laughter), (footsteps), …. */\n tagAudioEvents?: boolean\n /** Maximum number of speakers in the audio (1..32). */\n numSpeakers?: number\n /** Timestamp granularity for words. */\n timestampsGranularity?: 'word' | 'character' | 'none'\n /** Enable speaker diarization. */\n diarize?: boolean\n /** Diarization threshold (requires `diarize=true` and no `numSpeakers`). */\n diarizationThreshold?: number\n /** Detect speaker roles (agent/customer). Requires diarize=true. */\n detectSpeakerRoles?: boolean\n /** Bias the model towards these keyterms (max 1000). */\n keyterms?: Array<string>\n /**\n * Entity detection: `'all'`, a category (`'pii'`, `'phi'`, `'pci'`,\n * `'other'`, `'offensive_language'`), or a specific entity type.\n */\n entityDetection?: string\n /** Redact entities from the transcript text. Must be a subset of `entityDetection`. */\n entityRedaction?: string\n /** How redacted entities are formatted. */\n entityRedactionMode?: string\n /** Whether to skip filler words / non-speech sounds (scribe_v2 only). */\n noVerbatim?: boolean\n /** Sampling temperature (0..2). */\n temperature?: number\n /** Deterministic sampling seed (0..2147483647). */\n seed?: number\n /** Use `false` for zero-retention mode (enterprise only). */\n enableLogging?: boolean\n /** Multi-channel audio with one speaker per channel. Max 5 channels. */\n useMultiChannel?: boolean\n /**\n * Hint for audio format. Use `'pcm_s16le_16'` to skip encoding for 16-bit\n * PCM @ 16kHz mono little-endian inputs (lower latency).\n */\n fileFormat?: 'pcm_s16le_16' | 'other'\n}\n\n/**\n * ElevenLabs speech-to-text adapter built on the official SDK's Scribe family.\n *\n * @example\n * ```ts\n * const adapter = elevenlabsTranscription('scribe_v1')\n * const result = await generateTranscription({\n * adapter,\n * audio: fileInput,\n * language: 'en',\n * })\n * ```\n */\nexport class ElevenLabsTranscriptionAdapter<\n TModel extends ElevenLabsTranscriptionModel,\n> extends BaseTranscriptionAdapter<\n TModel,\n ElevenLabsTranscriptionProviderOptions\n> {\n readonly name = 'elevenlabs' as const\n\n private client: ElevenLabsClient\n\n constructor(model: TModel, config?: ElevenLabsClientConfig) {\n super(model, config ?? {})\n this.client = createElevenLabsClient(config)\n }\n\n async transcribe(\n options: TranscriptionOptions<ElevenLabsTranscriptionProviderOptions>,\n ): Promise<TranscriptionResult> {\n const { logger } = options\n logger.request(\n `activity=generateTranscription provider=elevenlabs model=${this.model}`,\n { provider: 'elevenlabs', model: this.model },\n )\n try {\n const modelOpts = options.modelOptions ?? {}\n const audioInput = normalizeAudioInput(options.audio)\n\n const response = await this.client.speechToText.convert({\n modelId: this.model,\n ...(audioInput.kind === 'file'\n ? { file: audioInput.value }\n : { cloudStorageUrl: audioInput.value }),\n ...(options.language ? { languageCode: options.language } : {}),\n ...(modelOpts.tagAudioEvents != null\n ? { tagAudioEvents: modelOpts.tagAudioEvents }\n : {}),\n ...(modelOpts.numSpeakers != null\n ? { numSpeakers: modelOpts.numSpeakers }\n : {}),\n ...(modelOpts.timestampsGranularity\n ? { timestampsGranularity: modelOpts.timestampsGranularity }\n : {}),\n ...(modelOpts.diarize != null ? { diarize: modelOpts.diarize } : {}),\n ...(modelOpts.diarizationThreshold != null\n ? { diarizationThreshold: modelOpts.diarizationThreshold }\n : {}),\n ...(modelOpts.detectSpeakerRoles != null\n ? { detectSpeakerRoles: modelOpts.detectSpeakerRoles }\n : {}),\n ...(modelOpts.keyterms ? { keyterms: modelOpts.keyterms } : {}),\n ...(modelOpts.entityDetection\n ? { entityDetection: modelOpts.entityDetection }\n : {}),\n ...(modelOpts.entityRedaction\n ? { entityRedaction: modelOpts.entityRedaction }\n : {}),\n ...(modelOpts.entityRedactionMode\n ? { entityRedactionMode: modelOpts.entityRedactionMode }\n : {}),\n ...(modelOpts.noVerbatim != null\n ? { noVerbatim: modelOpts.noVerbatim }\n : {}),\n ...(modelOpts.temperature != null\n ? { temperature: modelOpts.temperature }\n : {}),\n ...(modelOpts.seed != null ? { seed: modelOpts.seed } : {}),\n ...(modelOpts.enableLogging != null\n ? { enableLogging: modelOpts.enableLogging }\n : {}),\n ...(modelOpts.useMultiChannel != null\n ? { useMultiChannel: modelOpts.useMultiChannel }\n : {}),\n ...(modelOpts.fileFormat ? { fileFormat: modelOpts.fileFormat } : {}),\n } as Parameters<ElevenLabsClient['speechToText']['convert']>[0])\n\n return this.transformResponse(response)\n } catch (error) {\n logger.errors('elevenlabs.generateTranscription fatal', {\n error,\n source: 'elevenlabs.generateTranscription',\n })\n throw error\n }\n }\n\n private transformResponse(\n response: Awaited<ReturnType<ElevenLabsClient['speechToText']['convert']>>,\n ): TranscriptionResult {\n // The SDK types this as a union of single- and multi-channel responses.\n // We treat multi-channel as \"join the channel transcripts\" — consumers\n // who care about per-channel detail can re-parse from `modelOptions`.\n const data = response as unknown as {\n text?: string\n languageCode?: string\n languageProbability?: number\n words?: Array<{\n text: string\n start?: number\n end?: number\n type: string\n speakerId?: string\n }>\n audioDurationSecs?: number\n transcripts?: Array<{\n text?: string\n languageCode?: string\n words?: Array<{\n text: string\n start?: number\n end?: number\n type: string\n speakerId?: string\n }>\n audioDurationSecs?: number\n }>\n }\n\n if (data.transcripts) {\n const joinedText = data.transcripts\n .map((t) => t.text ?? '')\n .filter(Boolean)\n .join('\\n')\n const joinedWords = data.transcripts.flatMap((t) => t.words ?? [])\n const duration = data.transcripts.reduce(\n (max, t) => Math.max(max, t.audioDurationSecs ?? 0),\n 0,\n )\n const firstLang = data.transcripts.find(\n (t) => t.languageCode,\n )?.languageCode\n return {\n id: generateId(this.name),\n model: this.model,\n text: joinedText,\n ...(firstLang ? { language: firstLang } : {}),\n ...(duration ? { duration } : {}),\n ...buildWordsAndSegments(joinedWords),\n }\n }\n\n return {\n id: generateId(this.name),\n model: this.model,\n text: data.text ?? '',\n ...(data.languageCode ? { language: data.languageCode } : {}),\n ...(data.audioDurationSecs ? { duration: data.audioDurationSecs } : {}),\n ...buildWordsAndSegments(data.words ?? []),\n }\n }\n\n protected override generateId(): string {\n return generateId(this.name)\n }\n}\n\ntype NormalizedAudio =\n | { kind: 'file'; value: Blob }\n | { kind: 'url'; value: string }\n\nfunction normalizeAudioInput(\n audio: TranscriptionOptions['audio'],\n): NormalizedAudio {\n if (audio instanceof ArrayBuffer) {\n return { kind: 'file', value: new Blob([audio]) }\n }\n if (typeof audio === 'string') {\n const blob = dataUrlToBlob(audio)\n if (blob) return { kind: 'file', value: blob }\n return { kind: 'url', value: audio }\n }\n // Blob or File both fit the SDK's `Uploadable` contract.\n return { kind: 'file', value: audio }\n}\n\nfunction buildWordsAndSegments(\n words: Array<{\n text: string\n start?: number\n end?: number\n type: string\n speakerId?: string\n }>,\n): {\n words?: Array<TranscriptionWord>\n segments?: Array<TranscriptionSegment>\n} {\n const timedWords = words.filter(\n (w) =>\n typeof w.start === 'number' &&\n typeof w.end === 'number' &&\n w.type !== 'spacing',\n )\n if (timedWords.length === 0) return {}\n\n const outWords: Array<TranscriptionWord> = timedWords.map((w) => ({\n word: w.text,\n start: w.start!,\n end: w.end!,\n }))\n\n // Group contiguous words that share a speaker into segments. If no speaker\n // is ever set, we still emit one segment per sentence-ish grouping.\n const segments: Array<TranscriptionSegment> = []\n let current: {\n start: number\n end: number\n text: string\n speaker?: string\n } | null = null\n\n for (const w of timedWords) {\n if (!current) {\n current = {\n start: w.start!,\n end: w.end!,\n text: w.text,\n ...(w.speakerId ? { speaker: w.speakerId } : {}),\n }\n continue\n }\n if (w.speakerId && current.speaker !== w.speakerId) {\n segments.push({ id: segments.length, ...current })\n current = {\n start: w.start!,\n end: w.end!,\n text: w.text,\n speaker: w.speakerId,\n }\n continue\n }\n current.end = w.end!\n current.text = current.text ? `${current.text} ${w.text}` : w.text\n }\n if (current) segments.push({ id: segments.length, ...current })\n\n return { words: outWords, segments }\n}\n\n/**\n * Create an ElevenLabs transcription adapter using `ELEVENLABS_API_KEY` from env.\n */\nexport function elevenlabsTranscription<\n TModel extends ElevenLabsTranscriptionModel,\n>(\n model: TModel,\n config?: ElevenLabsClientConfig,\n): ElevenLabsTranscriptionAdapter<TModel> {\n return new ElevenLabsTranscriptionAdapter(model, config)\n}\n\n/**\n * Create an ElevenLabs transcription adapter with an explicit API key.\n */\nexport function createElevenLabsTranscription<\n TModel extends ElevenLabsTranscriptionModel,\n>(\n model: TModel,\n apiKey: string,\n config?: Omit<ElevenLabsClientConfig, 'apiKey'>,\n): ElevenLabsTranscriptionAdapter<TModel> {\n return new ElevenLabsTranscriptionAdapter(model, { apiKey, ...config })\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transcription.js","sources":["../../../src/adapters/transcription.ts"],"sourcesContent":["import { BaseTranscriptionAdapter } from '@tanstack/ai/adapters'\nimport {\n createElevenLabsClient,\n dataUrlToBlob,\n generateId,\n} from '../utils/client'\nimport type { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'\nimport type {\n TranscriptionOptions,\n TranscriptionResult,\n TranscriptionSegment,\n TranscriptionWord,\n} from '@tanstack/ai'\nimport type { ElevenLabsClientConfig } from '../utils/client'\nimport type { ElevenLabsTranscriptionModel } from '../model-meta'\n\n/**\n * Provider-specific options for ElevenLabs Scribe transcription. Fields map\n * 1:1 onto the SDK's `BodySpeechToTextV1SpeechToTextPost` — mirroring the\n * names so documentation stays useful.\n * @see https://elevenlabs.io/docs/api-reference/speech-to-text/convert\n */\nexport interface ElevenLabsTranscriptionProviderOptions {\n /** Annotate non-speech events like (laughter), (footsteps), …. */\n tagAudioEvents?: boolean\n /** Maximum number of speakers in the audio (1..32). */\n numSpeakers?: number\n /** Timestamp granularity for words. */\n timestampsGranularity?: 'word' | 'character' | 'none'\n /** Enable speaker diarization. */\n diarize?: boolean\n /** Diarization threshold (requires `diarize=true` and no `numSpeakers`). */\n diarizationThreshold?: number\n /** Detect speaker roles (agent/customer). Requires diarize=true. */\n detectSpeakerRoles?: boolean\n /** Bias the model towards these keyterms (max 1000). */\n keyterms?: Array<string>\n /**\n * Entity detection: `'all'`, a category (`'pii'`, `'phi'`, `'pci'`,\n * `'other'`, `'offensive_language'`), or a specific entity type.\n */\n entityDetection?: string\n /** Redact entities from the transcript text. Must be a subset of `entityDetection`. */\n entityRedaction?: string\n /** How redacted entities are formatted. */\n entityRedactionMode?: string\n /** Whether to skip filler words / non-speech sounds (scribe_v2 only). */\n noVerbatim?: boolean\n /** Sampling temperature (0..2). */\n temperature?: number\n /** Deterministic sampling seed (0..2147483647). */\n seed?: number\n /** Use `false` for zero-retention mode (enterprise only). */\n enableLogging?: boolean\n /** Multi-channel audio with one speaker per channel. Max 5 channels. */\n useMultiChannel?: boolean\n /**\n * Hint for audio format. Use `'pcm_s16le_16'` to skip encoding for 16-bit\n * PCM @ 16kHz mono little-endian inputs (lower latency).\n */\n fileFormat?: 'pcm_s16le_16' | 'other'\n}\n\n/**\n * ElevenLabs speech-to-text adapter built on the official SDK's Scribe family.\n *\n * @example\n * ```ts\n * const adapter = elevenlabsTranscription('scribe_v1')\n * const result = await generateTranscription({\n * adapter,\n * audio: fileInput,\n * language: 'en',\n * })\n * ```\n */\nexport class ElevenLabsTranscriptionAdapter<\n TModel extends ElevenLabsTranscriptionModel,\n> extends BaseTranscriptionAdapter<\n TModel,\n ElevenLabsTranscriptionProviderOptions\n> {\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 transcribe(\n options: TranscriptionOptions<ElevenLabsTranscriptionProviderOptions>,\n ): Promise<TranscriptionResult> {\n const { logger } = options\n logger.request(\n `activity=generateTranscription provider=elevenlabs model=${this.model}`,\n { provider: 'elevenlabs', model: this.model },\n )\n try {\n const modelOpts = options.modelOptions ?? {}\n const audioInput = normalizeAudioInput(options.audio)\n\n const response = await this.client.speechToText.convert({\n modelId: this.model,\n ...(audioInput.kind === 'file'\n ? { file: audioInput.value }\n : { cloudStorageUrl: audioInput.value }),\n ...(options.language ? { languageCode: options.language } : {}),\n ...(modelOpts.tagAudioEvents != null\n ? { tagAudioEvents: modelOpts.tagAudioEvents }\n : {}),\n ...(modelOpts.numSpeakers != null\n ? { numSpeakers: modelOpts.numSpeakers }\n : {}),\n ...(modelOpts.timestampsGranularity\n ? { timestampsGranularity: modelOpts.timestampsGranularity }\n : {}),\n ...(modelOpts.diarize != null ? { diarize: modelOpts.diarize } : {}),\n ...(modelOpts.diarizationThreshold != null\n ? { diarizationThreshold: modelOpts.diarizationThreshold }\n : {}),\n ...(modelOpts.detectSpeakerRoles != null\n ? { detectSpeakerRoles: modelOpts.detectSpeakerRoles }\n : {}),\n ...(modelOpts.keyterms ? { keyterms: modelOpts.keyterms } : {}),\n ...(modelOpts.entityDetection\n ? { entityDetection: modelOpts.entityDetection }\n : {}),\n ...(modelOpts.entityRedaction\n ? { entityRedaction: modelOpts.entityRedaction }\n : {}),\n ...(modelOpts.entityRedactionMode\n ? { entityRedactionMode: modelOpts.entityRedactionMode }\n : {}),\n ...(modelOpts.noVerbatim != null\n ? { noVerbatim: modelOpts.noVerbatim }\n : {}),\n ...(modelOpts.temperature != null\n ? { temperature: modelOpts.temperature }\n : {}),\n ...(modelOpts.seed != null ? { seed: modelOpts.seed } : {}),\n ...(modelOpts.enableLogging != null\n ? { enableLogging: modelOpts.enableLogging }\n : {}),\n ...(modelOpts.useMultiChannel != null\n ? { useMultiChannel: modelOpts.useMultiChannel }\n : {}),\n ...(modelOpts.fileFormat ? { fileFormat: modelOpts.fileFormat } : {}),\n } as Parameters<ElevenLabsClient['speechToText']['convert']>[0])\n\n return this.transformResponse(response)\n } catch (error) {\n logger.errors('elevenlabs.generateTranscription fatal', {\n error,\n source: 'elevenlabs.generateTranscription',\n })\n throw error\n }\n }\n\n private transformResponse(\n response: Awaited<ReturnType<ElevenLabsClient['speechToText']['convert']>>,\n ): TranscriptionResult {\n // The SDK types this as a union of single- and multi-channel responses.\n // We treat multi-channel as \"join the channel transcripts\" — consumers\n // who care about per-channel detail can re-parse from `modelOptions`.\n // eslint-disable-next-line no-restricted-syntax -- bridges SpeechToTextConvertResponse union (incl. webhook variant with no text/words/transcripts) to a flattened duck-typed shape we discriminate at runtime\n const data = response as unknown as {\n text?: string\n languageCode?: string\n languageProbability?: number\n words?: Array<{\n text: string\n start?: number\n end?: number\n type: string\n speakerId?: string\n }>\n audioDurationSecs?: number\n transcripts?: Array<{\n text?: string\n languageCode?: string\n words?: Array<{\n text: string\n start?: number\n end?: number\n type: string\n speakerId?: string\n }>\n audioDurationSecs?: number\n }>\n }\n\n if (data.transcripts) {\n const joinedText = data.transcripts\n .map((t) => t.text ?? '')\n .filter(Boolean)\n .join('\\n')\n const joinedWords = data.transcripts.flatMap((t) => t.words ?? [])\n const duration = data.transcripts.reduce(\n (max, t) => Math.max(max, t.audioDurationSecs ?? 0),\n 0,\n )\n const firstLang = data.transcripts.find(\n (t) => t.languageCode,\n )?.languageCode\n return {\n id: generateId(this.name),\n model: this.model,\n text: joinedText,\n ...(firstLang ? { language: firstLang } : {}),\n ...(duration ? { duration } : {}),\n ...buildWordsAndSegments(joinedWords),\n }\n }\n\n return {\n id: generateId(this.name),\n model: this.model,\n text: data.text ?? '',\n ...(data.languageCode ? { language: data.languageCode } : {}),\n ...(data.audioDurationSecs ? { duration: data.audioDurationSecs } : {}),\n ...buildWordsAndSegments(data.words ?? []),\n }\n }\n\n protected override generateId(): string {\n return generateId(this.name)\n }\n}\n\ntype NormalizedAudio =\n | { kind: 'file'; value: Blob }\n | { kind: 'url'; value: string }\n\nfunction normalizeAudioInput(\n audio: TranscriptionOptions['audio'],\n): NormalizedAudio {\n if (audio instanceof ArrayBuffer) {\n return { kind: 'file', value: new Blob([audio]) }\n }\n if (typeof audio === 'string') {\n const blob = dataUrlToBlob(audio)\n if (blob) return { kind: 'file', value: blob }\n return { kind: 'url', value: audio }\n }\n // Blob or File both fit the SDK's `Uploadable` contract.\n return { kind: 'file', value: audio }\n}\n\nfunction buildWordsAndSegments(\n words: Array<{\n text: string\n start?: number\n end?: number\n type: string\n speakerId?: string\n }>,\n): {\n words?: Array<TranscriptionWord>\n segments?: Array<TranscriptionSegment>\n} {\n const timedWords = words.filter(\n (w): w is typeof w & { start: number; end: number } =>\n typeof w.start === 'number' &&\n typeof w.end === 'number' &&\n w.type !== 'spacing',\n )\n if (timedWords.length === 0) return {}\n\n const outWords: Array<TranscriptionWord> = timedWords.map((w) => ({\n word: w.text,\n start: w.start,\n end: w.end,\n }))\n\n // Group contiguous words that share a speaker into segments. If no speaker\n // is ever set, we still emit one segment per sentence-ish grouping.\n const segments: Array<TranscriptionSegment> = []\n let current: {\n start: number\n end: number\n text: string\n speaker?: string\n } | null = null\n\n for (const w of timedWords) {\n if (!current) {\n current = {\n start: w.start,\n end: w.end,\n text: w.text,\n ...(w.speakerId ? { speaker: w.speakerId } : {}),\n }\n continue\n }\n if (w.speakerId && current.speaker !== w.speakerId) {\n segments.push({ id: segments.length, ...current })\n current = {\n start: w.start,\n end: w.end,\n text: w.text,\n speaker: w.speakerId,\n }\n continue\n }\n current.end = w.end\n current.text = current.text ? `${current.text} ${w.text}` : w.text\n }\n if (current) segments.push({ id: segments.length, ...current })\n\n return { words: outWords, segments }\n}\n\n/**\n * Create an ElevenLabs transcription adapter using `ELEVENLABS_API_KEY` from env.\n */\nexport function elevenlabsTranscription<\n TModel extends ElevenLabsTranscriptionModel,\n>(\n model: TModel,\n config?: ElevenLabsClientConfig,\n): ElevenLabsTranscriptionAdapter<TModel> {\n return new ElevenLabsTranscriptionAdapter(model, config)\n}\n\n/**\n * Create an ElevenLabs transcription adapter with an explicit API key.\n */\nexport function createElevenLabsTranscription<\n TModel extends ElevenLabsTranscriptionModel,\n>(\n model: TModel,\n apiKey: string,\n config?: Omit<ElevenLabsClientConfig, 'apiKey'>,\n): ElevenLabsTranscriptionAdapter<TModel> {\n return new ElevenLabsTranscriptionAdapter(model, { apiKey, ...config })\n}\n"],"names":[],"mappings":";;AA4EO,MAAM,uCAEH,yBAGR;AAAA,EACS,OAAO;AAAA,EAEC;AAAA,EAEjB,YAAY,OAAe,QAAiC;AAC1D,UAAM,OAAO,UAAU,EAAE;AACzB,SAAK,SAAS,uBAAuB,MAAM;AAAA,EAC7C;AAAA,EAEA,MAAM,WACJ,SAC8B;AAC9B,UAAM,EAAE,WAAW;AACnB,WAAO;AAAA,MACL,4DAA4D,KAAK,KAAK;AAAA,MACtE,EAAE,UAAU,cAAc,OAAO,KAAK,MAAA;AAAA,IAAM;AAE9C,QAAI;AACF,YAAM,YAAY,QAAQ,gBAAgB,CAAA;AAC1C,YAAM,aAAa,oBAAoB,QAAQ,KAAK;AAEpD,YAAM,WAAW,MAAM,KAAK,OAAO,aAAa,QAAQ;AAAA,QACtD,SAAS,KAAK;AAAA,QACd,GAAI,WAAW,SAAS,SACpB,EAAE,MAAM,WAAW,UACnB,EAAE,iBAAiB,WAAW,MAAA;AAAA,QAClC,GAAI,QAAQ,WAAW,EAAE,cAAc,QAAQ,SAAA,IAAa,CAAA;AAAA,QAC5D,GAAI,UAAU,kBAAkB,OAC5B,EAAE,gBAAgB,UAAU,eAAA,IAC5B,CAAA;AAAA,QACJ,GAAI,UAAU,eAAe,OACzB,EAAE,aAAa,UAAU,YAAA,IACzB,CAAA;AAAA,QACJ,GAAI,UAAU,wBACV,EAAE,uBAAuB,UAAU,sBAAA,IACnC,CAAA;AAAA,QACJ,GAAI,UAAU,WAAW,OAAO,EAAE,SAAS,UAAU,QAAA,IAAY,CAAA;AAAA,QACjE,GAAI,UAAU,wBAAwB,OAClC,EAAE,sBAAsB,UAAU,qBAAA,IAClC,CAAA;AAAA,QACJ,GAAI,UAAU,sBAAsB,OAChC,EAAE,oBAAoB,UAAU,mBAAA,IAChC,CAAA;AAAA,QACJ,GAAI,UAAU,WAAW,EAAE,UAAU,UAAU,SAAA,IAAa,CAAA;AAAA,QAC5D,GAAI,UAAU,kBACV,EAAE,iBAAiB,UAAU,gBAAA,IAC7B,CAAA;AAAA,QACJ,GAAI,UAAU,kBACV,EAAE,iBAAiB,UAAU,gBAAA,IAC7B,CAAA;AAAA,QACJ,GAAI,UAAU,sBACV,EAAE,qBAAqB,UAAU,oBAAA,IACjC,CAAA;AAAA,QACJ,GAAI,UAAU,cAAc,OACxB,EAAE,YAAY,UAAU,WAAA,IACxB,CAAA;AAAA,QACJ,GAAI,UAAU,eAAe,OACzB,EAAE,aAAa,UAAU,YAAA,IACzB,CAAA;AAAA,QACJ,GAAI,UAAU,QAAQ,OAAO,EAAE,MAAM,UAAU,KAAA,IAAS,CAAA;AAAA,QACxD,GAAI,UAAU,iBAAiB,OAC3B,EAAE,eAAe,UAAU,cAAA,IAC3B,CAAA;AAAA,QACJ,GAAI,UAAU,mBAAmB,OAC7B,EAAE,iBAAiB,UAAU,gBAAA,IAC7B,CAAA;AAAA,QACJ,GAAI,UAAU,aAAa,EAAE,YAAY,UAAU,WAAA,IAAe,CAAA;AAAA,MAAC,CACN;AAE/D,aAAO,KAAK,kBAAkB,QAAQ;AAAA,IACxC,SAAS,OAAO;AACd,aAAO,OAAO,0CAA0C;AAAA,QACtD;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,kBACN,UACqB;AAKrB,UAAM,OAAO;AA0Bb,QAAI,KAAK,aAAa;AACpB,YAAM,aAAa,KAAK,YACrB,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,EACvB,OAAO,OAAO,EACd,KAAK,IAAI;AACZ,YAAM,cAAc,KAAK,YAAY,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE;AACjE,YAAM,WAAW,KAAK,YAAY;AAAA,QAChC,CAAC,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,qBAAqB,CAAC;AAAA,QAClD;AAAA,MAAA;AAEF,YAAM,YAAY,KAAK,YAAY;AAAA,QACjC,CAAC,MAAM,EAAE;AAAA,MAAA,GACR;AACH,aAAO;AAAA,QACL,IAAI,WAAW,KAAK,IAAI;AAAA,QACxB,OAAO,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,GAAI,YAAY,EAAE,UAAU,UAAA,IAAc,CAAA;AAAA,QAC1C,GAAI,WAAW,EAAE,SAAA,IAAa,CAAA;AAAA,QAC9B,GAAG,sBAAsB,WAAW;AAAA,MAAA;AAAA,IAExC;AAEA,WAAO;AAAA,MACL,IAAI,WAAW,KAAK,IAAI;AAAA,MACxB,OAAO,KAAK;AAAA,MACZ,MAAM,KAAK,QAAQ;AAAA,MACnB,GAAI,KAAK,eAAe,EAAE,UAAU,KAAK,aAAA,IAAiB,CAAA;AAAA,MAC1D,GAAI,KAAK,oBAAoB,EAAE,UAAU,KAAK,kBAAA,IAAsB,CAAA;AAAA,MACpE,GAAG,sBAAsB,KAAK,SAAS,CAAA,CAAE;AAAA,IAAA;AAAA,EAE7C;AAAA,EAEmB,aAAqB;AACtC,WAAO,WAAW,KAAK,IAAI;AAAA,EAC7B;AACF;AAMA,SAAS,oBACP,OACiB;AACjB,MAAI,iBAAiB,aAAa;AAChC,WAAO,EAAE,MAAM,QAAQ,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,EAAA;AAAA,EAChD;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,OAAO,cAAc,KAAK;AAChC,QAAI,KAAM,QAAO,EAAE,MAAM,QAAQ,OAAO,KAAA;AACxC,WAAO,EAAE,MAAM,OAAO,OAAO,MAAA;AAAA,EAC/B;AAEA,SAAO,EAAE,MAAM,QAAQ,OAAO,MAAA;AAChC;AAEA,SAAS,sBACP,OAUA;AACA,QAAM,aAAa,MAAM;AAAA,IACvB,CAAC,MACC,OAAO,EAAE,UAAU,YACnB,OAAO,EAAE,QAAQ,YACjB,EAAE,SAAS;AAAA,EAAA;AAEf,MAAI,WAAW,WAAW,EAAG,QAAO,CAAA;AAEpC,QAAM,WAAqC,WAAW,IAAI,CAAC,OAAO;AAAA,IAChE,MAAM,EAAE;AAAA,IACR,OAAO,EAAE;AAAA,IACT,KAAK,EAAE;AAAA,EAAA,EACP;AAIF,QAAM,WAAwC,CAAA;AAC9C,MAAI,UAKO;AAEX,aAAW,KAAK,YAAY;AAC1B,QAAI,CAAC,SAAS;AACZ,gBAAU;AAAA,QACR,OAAO,EAAE;AAAA,QACT,KAAK,EAAE;AAAA,QACP,MAAM,EAAE;AAAA,QACR,GAAI,EAAE,YAAY,EAAE,SAAS,EAAE,UAAA,IAAc,CAAA;AAAA,MAAC;AAEhD;AAAA,IACF;AACA,QAAI,EAAE,aAAa,QAAQ,YAAY,EAAE,WAAW;AAClD,eAAS,KAAK,EAAE,IAAI,SAAS,QAAQ,GAAG,SAAS;AACjD,gBAAU;AAAA,QACR,OAAO,EAAE;AAAA,QACT,KAAK,EAAE;AAAA,QACP,MAAM,EAAE;AAAA,QACR,SAAS,EAAE;AAAA,MAAA;AAEb;AAAA,IACF;AACA,YAAQ,MAAM,EAAE;AAChB,YAAQ,OAAO,QAAQ,OAAO,GAAG,QAAQ,IAAI,IAAI,EAAE,IAAI,KAAK,EAAE;AAAA,EAChE;AACA,MAAI,kBAAkB,KAAK,EAAE,IAAI,SAAS,QAAQ,GAAG,SAAS;AAE9D,SAAO,EAAE,OAAO,UAAU,SAAA;AAC5B;AAKO,SAAS,wBAGd,OACA,QACwC;AACxC,SAAO,IAAI,+BAA+B,OAAO,MAAM;AACzD;AAKO,SAAS,8BAGd,OACA,QACA,QACwC;AACxC,SAAO,IAAI,+BAA+B,OAAO,EAAE,QAAQ,GAAG,QAAQ;AACxE;"}
@@ -151,10 +151,12 @@ async function createElevenLabsConnection(token, _options, logger, clientToolDef
151
151
  emit("interrupted", {});
152
152
  },
153
153
  on(event, handler) {
154
- if (!eventHandlers.has(event)) {
155
- eventHandlers.set(event, /* @__PURE__ */ new Set());
154
+ let handlers = eventHandlers.get(event);
155
+ if (!handlers) {
156
+ handlers = /* @__PURE__ */ new Set();
157
+ eventHandlers.set(event, handlers);
156
158
  }
157
- eventHandlers.get(event).add(handler);
159
+ handlers.add(handler);
158
160
  return () => {
159
161
  eventHandlers.get(event)?.delete(handler);
160
162
  };
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.js","sources":["../../../src/realtime/adapter.ts"],"sourcesContent":["import { Conversation } from '@elevenlabs/client'\nimport { resolveDebugOption } from '@tanstack/ai/adapter-internals'\nimport type {\n AnyClientTool,\n AudioVisualization,\n RealtimeEvent,\n RealtimeEventHandler,\n RealtimeMessage,\n RealtimeMode,\n RealtimeSessionConfig,\n RealtimeStatus,\n RealtimeToken,\n} from '@tanstack/ai'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai-client'\nimport type { ElevenLabsRealtimeOptions } from './types'\n\n/**\n * Creates an ElevenLabs realtime adapter for client-side use.\n *\n * Wraps the @elevenlabs/client SDK for voice conversations.\n *\n * @param options - Optional configuration\n * @returns A RealtimeAdapter for use with RealtimeClient\n *\n * @example\n * ```typescript\n * import { RealtimeClient } from '@tanstack/ai-client'\n * import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'\n *\n * const client = new RealtimeClient({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: elevenlabsRealtime(),\n * })\n * ```\n */\nexport function elevenlabsRealtime(\n options: ElevenLabsRealtimeOptions = {},\n): RealtimeAdapter {\n const logger = resolveDebugOption(options.debug)\n\n return {\n provider: 'elevenlabs',\n\n async connect(\n token: RealtimeToken,\n clientToolDefs?: ReadonlyArray<AnyClientTool>,\n ): Promise<RealtimeConnection> {\n logger.request(`activity=realtime provider=elevenlabs`, {\n provider: 'elevenlabs',\n })\n return createElevenLabsConnection(token, options, logger, clientToolDefs)\n },\n }\n}\n\n/**\n * Creates a connection to ElevenLabs conversational AI\n */\nasync function createElevenLabsConnection(\n token: RealtimeToken,\n _options: ElevenLabsRealtimeOptions,\n logger: InternalLogger,\n clientToolDefs?: ReadonlyArray<AnyClientTool>,\n): Promise<RealtimeConnection> {\n const eventHandlers = new Map<RealtimeEvent, Set<RealtimeEventHandler<any>>>()\n let conversation: Awaited<\n ReturnType<typeof Conversation.startSession>\n > | null = null\n let messageIdCounter = 0\n\n // Empty arrays for when visualization isn't available\n const emptyFrequencyData = new Uint8Array(128)\n const emptyTimeDomainData = new Uint8Array(128).fill(128)\n\n // Helper to emit events\n function emit<TEvent extends RealtimeEvent>(\n event: TEvent,\n payload: Parameters<RealtimeEventHandler<TEvent>>[0],\n ) {\n const handlers = eventHandlers.get(event)\n if (handlers) {\n for (const handler of handlers) {\n handler(payload)\n }\n }\n }\n\n function generateMessageId(): string {\n return `el-msg-${Date.now()}-${++messageIdCounter}`\n }\n\n // Convert TanStack tool definitions to ElevenLabs clientTools format.\n // @elevenlabs/client expects plain async functions, not objects.\n const elevenLabsClientTools: Record<\n string,\n (params: unknown) => Promise<string>\n > = {}\n\n if (clientToolDefs) {\n for (const tool of clientToolDefs) {\n elevenLabsClientTools[tool.name] = async (params: unknown) => {\n if (tool.execute) {\n const result = await tool.execute(params)\n return typeof result === 'string' ? result : JSON.stringify(result)\n }\n return JSON.stringify({\n error: `No execute function for tool ${tool.name}`,\n })\n }\n }\n }\n\n // Build session options\n const sessionOptions: Record<string, unknown> = {\n signedUrl: token.token,\n\n onConnect: () => {\n logger.provider(`provider=elevenlabs direction=in type=connect`, {\n frame: { type: 'connect' },\n })\n emit('status_change', { status: 'connected' as RealtimeStatus })\n emit('mode_change', { mode: 'listening' })\n },\n\n onDisconnect: () => {\n logger.provider(`provider=elevenlabs direction=in type=disconnect`, {\n frame: { type: 'disconnect' },\n })\n emit('status_change', { status: 'idle' as RealtimeStatus })\n emit('mode_change', { mode: 'idle' })\n },\n\n onModeChange: ({ mode }: { mode: string }) => {\n logger.provider(`provider=elevenlabs direction=in type=mode_change`, {\n frame: { type: 'mode_change', mode },\n })\n const mappedMode: RealtimeMode =\n mode === 'speaking' ? 'speaking' : 'listening'\n emit('mode_change', { mode: mappedMode })\n },\n\n onMessage: ({ message, source }: { message: string; source: string }) => {\n logger.provider(`provider=elevenlabs direction=in type=message`, {\n frame: { type: 'message', source, message },\n })\n const role = source === 'user' ? 'user' : 'assistant'\n\n if (role === 'user') {\n // User transcripts: only emit transcript — the RealtimeClient\n // creates the user message when it receives a final transcript\n emit('transcript', {\n role,\n transcript: message,\n isFinal: true,\n })\n } else {\n // Assistant messages: only emit message_complete — the canonical\n // event for finalized assistant messages\n const realtimeMessage: RealtimeMessage = {\n id: generateMessageId(),\n role,\n timestamp: Date.now(),\n parts: [{ type: 'audio', transcript: message }],\n }\n emit('message_complete', { message: realtimeMessage })\n }\n },\n\n onError: (error: string | Error) => {\n logger.errors('elevenlabs.realtime fatal', {\n error,\n source: 'elevenlabs.realtime',\n })\n emit('error', {\n error: new Error(\n typeof error === 'string' ? error : error.message || 'Unknown error',\n ),\n })\n },\n }\n\n // Only add clientTools if we have any\n if (Object.keys(elevenLabsClientTools).length > 0) {\n sessionOptions.clientTools = elevenLabsClientTools\n }\n\n // Start the conversation session\n try {\n conversation = await Conversation.startSession(\n sessionOptions as Parameters<typeof Conversation.startSession>[0],\n )\n } catch (error) {\n logger.errors('elevenlabs.realtime fatal', {\n error,\n source: 'elevenlabs.realtime',\n })\n throw error\n }\n\n // Connection implementation\n const connection: RealtimeConnection = {\n async disconnect() {\n if (conversation) {\n await conversation.endSession()\n conversation = null\n }\n emit('status_change', { status: 'idle' as RealtimeStatus })\n },\n\n async startAudioCapture() {\n // ElevenLabs SDK handles audio capture automatically\n // This is called when the session starts\n emit('mode_change', { mode: 'listening' })\n },\n\n stopAudioCapture() {\n // ElevenLabs SDK handles this\n emit('mode_change', { mode: 'idle' })\n },\n\n sendText(text: string) {\n if (!conversation) return\n logger.provider(`provider=elevenlabs direction=out type=user_message`, {\n frame: { type: 'user_message', text },\n })\n conversation.sendUserMessage(text)\n },\n\n sendImage(_imageData: string, _mimeType: string) {\n // ElevenLabs does not support direct image input in the conversation API\n logger.errors('elevenlabs.realtime sendImage not supported', {\n source: 'elevenlabs.realtime',\n })\n },\n\n sendToolResult(_callId: string, _result: string) {\n // ElevenLabs client tools are handled via the clientTools handlers\n // registered at session start — results are returned automatically\n },\n\n updateSession(_config: Partial<RealtimeSessionConfig>) {\n // ElevenLabs session config is set at creation time\n logger.errors('elevenlabs.realtime updateSession not supported', {\n source: 'elevenlabs.realtime',\n })\n },\n\n interrupt() {\n // ElevenLabs handles interruption automatically via barge-in\n // No explicit API to call\n emit('mode_change', { mode: 'listening' })\n emit('interrupted', {})\n },\n\n on<TEvent extends RealtimeEvent>(\n event: TEvent,\n handler: RealtimeEventHandler<TEvent>,\n ): () => void {\n if (!eventHandlers.has(event)) {\n eventHandlers.set(event, new Set())\n }\n eventHandlers.get(event)!.add(handler)\n\n return () => {\n eventHandlers.get(event)?.delete(handler)\n }\n },\n\n getAudioVisualization(): AudioVisualization {\n return {\n get inputLevel() {\n if (!conversation) return 0\n try {\n return conversation.getInputVolume()\n } catch {\n return 0\n }\n },\n\n get outputLevel() {\n if (!conversation) return 0\n try {\n return conversation.getOutputVolume()\n } catch {\n return 0\n }\n },\n\n getInputFrequencyData() {\n if (!conversation) return emptyFrequencyData\n try {\n return conversation.getInputByteFrequencyData()\n } catch {\n return emptyFrequencyData\n }\n },\n\n getOutputFrequencyData() {\n if (!conversation) return emptyFrequencyData\n try {\n return conversation.getOutputByteFrequencyData()\n } catch {\n return emptyFrequencyData\n }\n },\n\n getInputTimeDomainData() {\n // ElevenLabs SDK doesn't expose time domain data\n return emptyTimeDomainData\n },\n\n getOutputTimeDomainData() {\n // ElevenLabs SDK doesn't expose time domain data\n return emptyTimeDomainData\n },\n\n get inputSampleRate() {\n return 16000\n },\n\n get outputSampleRate() {\n return 16000\n },\n }\n },\n }\n\n return connection\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.js","sources":["../../../src/realtime/adapter.ts"],"sourcesContent":["import { Conversation } from '@elevenlabs/client'\nimport { resolveDebugOption } from '@tanstack/ai/adapter-internals'\nimport type {\n AnyClientTool,\n AudioVisualization,\n RealtimeEvent,\n RealtimeEventHandler,\n RealtimeMessage,\n RealtimeMode,\n RealtimeSessionConfig,\n RealtimeToken,\n} from '@tanstack/ai'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai-client'\nimport type { ElevenLabsRealtimeOptions } from './types'\n\n/**\n * Creates an ElevenLabs realtime adapter for client-side use.\n *\n * Wraps the @elevenlabs/client SDK for voice conversations.\n *\n * @param options - Optional configuration\n * @returns A RealtimeAdapter for use with RealtimeClient\n *\n * @example\n * ```typescript\n * import { RealtimeClient } from '@tanstack/ai-client'\n * import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'\n *\n * const client = new RealtimeClient({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: elevenlabsRealtime(),\n * })\n * ```\n */\nexport function elevenlabsRealtime(\n options: ElevenLabsRealtimeOptions = {},\n): RealtimeAdapter {\n const logger = resolveDebugOption(options.debug)\n\n return {\n provider: 'elevenlabs',\n\n async connect(\n token: RealtimeToken,\n clientToolDefs?: ReadonlyArray<AnyClientTool>,\n ): Promise<RealtimeConnection> {\n logger.request(`activity=realtime provider=elevenlabs`, {\n provider: 'elevenlabs',\n })\n return createElevenLabsConnection(token, options, logger, clientToolDefs)\n },\n }\n}\n\n/**\n * Creates a connection to ElevenLabs conversational AI\n */\nasync function createElevenLabsConnection(\n token: RealtimeToken,\n _options: ElevenLabsRealtimeOptions,\n logger: InternalLogger,\n clientToolDefs?: ReadonlyArray<AnyClientTool>,\n): Promise<RealtimeConnection> {\n const eventHandlers = new Map<RealtimeEvent, Set<RealtimeEventHandler<any>>>()\n let conversation: Awaited<\n ReturnType<typeof Conversation.startSession>\n > | null = null\n let messageIdCounter = 0\n\n // Empty arrays for when visualization isn't available\n const emptyFrequencyData = new Uint8Array(128)\n const emptyTimeDomainData = new Uint8Array(128).fill(128)\n\n // Helper to emit events\n function emit<TEvent extends RealtimeEvent>(\n event: TEvent,\n payload: Parameters<RealtimeEventHandler<TEvent>>[0],\n ) {\n const handlers = eventHandlers.get(event)\n if (handlers) {\n for (const handler of handlers) {\n handler(payload)\n }\n }\n }\n\n function generateMessageId(): string {\n return `el-msg-${Date.now()}-${++messageIdCounter}`\n }\n\n // Convert TanStack tool definitions to ElevenLabs clientTools format.\n // @elevenlabs/client expects plain async functions, not objects.\n const elevenLabsClientTools: Record<\n string,\n (params: unknown) => Promise<string>\n > = {}\n\n if (clientToolDefs) {\n for (const tool of clientToolDefs) {\n elevenLabsClientTools[tool.name] = async (params: unknown) => {\n if (tool.execute) {\n const result = await tool.execute(params)\n return typeof result === 'string' ? result : JSON.stringify(result)\n }\n return JSON.stringify({\n error: `No execute function for tool ${tool.name}`,\n })\n }\n }\n }\n\n // Build session options\n const sessionOptions: Record<string, unknown> = {\n signedUrl: token.token,\n\n onConnect: () => {\n logger.provider(`provider=elevenlabs direction=in type=connect`, {\n frame: { type: 'connect' },\n })\n emit('status_change', { status: 'connected' })\n emit('mode_change', { mode: 'listening' })\n },\n\n onDisconnect: () => {\n logger.provider(`provider=elevenlabs direction=in type=disconnect`, {\n frame: { type: 'disconnect' },\n })\n emit('status_change', { status: 'idle' })\n emit('mode_change', { mode: 'idle' })\n },\n\n onModeChange: ({ mode }: { mode: string }) => {\n logger.provider(`provider=elevenlabs direction=in type=mode_change`, {\n frame: { type: 'mode_change', mode },\n })\n const mappedMode: RealtimeMode =\n mode === 'speaking' ? 'speaking' : 'listening'\n emit('mode_change', { mode: mappedMode })\n },\n\n onMessage: ({ message, source }: { message: string; source: string }) => {\n logger.provider(`provider=elevenlabs direction=in type=message`, {\n frame: { type: 'message', source, message },\n })\n const role = source === 'user' ? 'user' : 'assistant'\n\n if (role === 'user') {\n // User transcripts: only emit transcript — the RealtimeClient\n // creates the user message when it receives a final transcript\n emit('transcript', {\n role,\n transcript: message,\n isFinal: true,\n })\n } else {\n // Assistant messages: only emit message_complete — the canonical\n // event for finalized assistant messages\n const realtimeMessage: RealtimeMessage = {\n id: generateMessageId(),\n role,\n timestamp: Date.now(),\n parts: [{ type: 'audio', transcript: message }],\n }\n emit('message_complete', { message: realtimeMessage })\n }\n },\n\n onError: (error: string | Error) => {\n logger.errors('elevenlabs.realtime fatal', {\n error,\n source: 'elevenlabs.realtime',\n })\n emit('error', {\n error: new Error(\n typeof error === 'string' ? error : error.message || 'Unknown error',\n ),\n })\n },\n }\n\n // Only add clientTools if we have any\n if (Object.keys(elevenLabsClientTools).length > 0) {\n sessionOptions.clientTools = elevenLabsClientTools\n }\n\n // Start the conversation session\n try {\n conversation = await Conversation.startSession(\n sessionOptions as Parameters<typeof Conversation.startSession>[0],\n )\n } catch (error) {\n logger.errors('elevenlabs.realtime fatal', {\n error,\n source: 'elevenlabs.realtime',\n })\n throw error\n }\n\n // Connection implementation\n const connection: RealtimeConnection = {\n async disconnect() {\n if (conversation) {\n await conversation.endSession()\n conversation = null\n }\n emit('status_change', { status: 'idle' })\n },\n\n async startAudioCapture() {\n // ElevenLabs SDK handles audio capture automatically\n // This is called when the session starts\n emit('mode_change', { mode: 'listening' })\n },\n\n stopAudioCapture() {\n // ElevenLabs SDK handles this\n emit('mode_change', { mode: 'idle' })\n },\n\n sendText(text: string) {\n if (!conversation) return\n logger.provider(`provider=elevenlabs direction=out type=user_message`, {\n frame: { type: 'user_message', text },\n })\n conversation.sendUserMessage(text)\n },\n\n sendImage(_imageData: string, _mimeType: string) {\n // ElevenLabs does not support direct image input in the conversation API\n logger.errors('elevenlabs.realtime sendImage not supported', {\n source: 'elevenlabs.realtime',\n })\n },\n\n sendToolResult(_callId: string, _result: string) {\n // ElevenLabs client tools are handled via the clientTools handlers\n // registered at session start — results are returned automatically\n },\n\n updateSession(_config: Partial<RealtimeSessionConfig>) {\n // ElevenLabs session config is set at creation time\n logger.errors('elevenlabs.realtime updateSession not supported', {\n source: 'elevenlabs.realtime',\n })\n },\n\n interrupt() {\n // ElevenLabs handles interruption automatically via barge-in\n // No explicit API to call\n emit('mode_change', { mode: 'listening' })\n emit('interrupted', {})\n },\n\n on<TEvent extends RealtimeEvent>(\n event: TEvent,\n handler: RealtimeEventHandler<TEvent>,\n ): () => void {\n let handlers = eventHandlers.get(event)\n if (!handlers) {\n handlers = new Set()\n eventHandlers.set(event, handlers)\n }\n handlers.add(handler)\n\n return () => {\n eventHandlers.get(event)?.delete(handler)\n }\n },\n\n getAudioVisualization(): AudioVisualization {\n return {\n get inputLevel() {\n if (!conversation) return 0\n try {\n return conversation.getInputVolume()\n } catch {\n return 0\n }\n },\n\n get outputLevel() {\n if (!conversation) return 0\n try {\n return conversation.getOutputVolume()\n } catch {\n return 0\n }\n },\n\n getInputFrequencyData() {\n if (!conversation) return emptyFrequencyData\n try {\n return conversation.getInputByteFrequencyData()\n } catch {\n return emptyFrequencyData\n }\n },\n\n getOutputFrequencyData() {\n if (!conversation) return emptyFrequencyData\n try {\n return conversation.getOutputByteFrequencyData()\n } catch {\n return emptyFrequencyData\n }\n },\n\n getInputTimeDomainData() {\n // ElevenLabs SDK doesn't expose time domain data\n return emptyTimeDomainData\n },\n\n getOutputTimeDomainData() {\n // ElevenLabs SDK doesn't expose time domain data\n return emptyTimeDomainData\n },\n\n get inputSampleRate() {\n return 16000\n },\n\n get outputSampleRate() {\n return 16000\n },\n }\n },\n }\n\n return connection\n}\n"],"names":[],"mappings":"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"}
@@ -15,12 +15,18 @@ function elevenlabsRealtimeToken(options = {}) {
15
15
  token: response.signedUrl,
16
16
  expiresAt,
17
17
  config: {
18
- voice: overrides?.voiceId,
19
- instructions: overrides?.systemPrompt,
18
+ ...overrides?.voiceId !== void 0 && { voice: overrides.voiceId },
19
+ ...overrides?.systemPrompt !== void 0 && {
20
+ instructions: overrides.systemPrompt
21
+ },
20
22
  providerOptions: {
21
23
  agentId,
22
- firstMessage: overrides?.firstMessage,
23
- language: overrides?.language
24
+ ...overrides?.firstMessage !== void 0 && {
25
+ firstMessage: overrides.firstMessage
26
+ },
27
+ ...overrides?.language !== void 0 && {
28
+ language: overrides.language
29
+ }
24
30
  }
25
31
  }
26
32
  };
@@ -1 +1 @@
1
- {"version":3,"file":"token.js","sources":["../../../src/realtime/token.ts"],"sourcesContent":["import {\n createElevenLabsClient,\n getElevenLabsAgentIdFromEnv,\n} from '../utils/client'\nimport type { RealtimeToken, RealtimeTokenAdapter } from '@tanstack/ai'\nimport type { ElevenLabsRealtimeTokenOptions } from './types'\n\n/**\n * Creates an ElevenLabs realtime token adapter.\n *\n * Uses the official `@elevenlabs/elevenlabs-js` SDK to request a signed URL\n * for client-side conversation connections. The signed URL is valid for\n * 30 minutes.\n *\n * @param options - Configuration. `agentId` falls back to\n * `ELEVENLABS_AGENT_ID` in the environment when omitted.\n * @returns A RealtimeTokenAdapter for use with realtimeToken()\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'\n *\n * // Reads ELEVENLABS_AGENT_ID from env:\n * const token = await realtimeToken({ adapter: elevenlabsRealtimeToken() })\n *\n * // Or pass explicitly:\n * const token = await realtimeToken({\n * adapter: elevenlabsRealtimeToken({ agentId: 'your-agent-id' }),\n * })\n * ```\n */\nexport function elevenlabsRealtimeToken(\n options: ElevenLabsRealtimeTokenOptions = {},\n): RealtimeTokenAdapter {\n const client = createElevenLabsClient()\n\n return {\n provider: 'elevenlabs',\n\n async generateToken(): Promise<RealtimeToken> {\n const { overrides } = options\n const agentId = options.agentId ?? getElevenLabsAgentIdFromEnv()\n\n const response = await client.conversationalAi.conversations.getSignedUrl(\n { agentId },\n )\n\n // Signed URLs are valid for 30 minutes\n const expiresAt = Date.now() + 30 * 60 * 1000\n\n return {\n provider: 'elevenlabs',\n token: response.signedUrl,\n expiresAt,\n config: {\n voice: overrides?.voiceId,\n instructions: overrides?.systemPrompt,\n providerOptions: {\n agentId,\n firstMessage: overrides?.firstMessage,\n language: overrides?.language,\n },\n },\n }\n },\n }\n}\n"],"names":[],"mappings":";AAgCO,SAAS,wBACd,UAA0C,IACpB;AACtB,QAAM,SAAS,uBAAA;AAEf,SAAO;AAAA,IACL,UAAU;AAAA,IAEV,MAAM,gBAAwC;AAC5C,YAAM,EAAE,cAAc;AACtB,YAAM,UAAU,QAAQ,WAAW,4BAAA;AAEnC,YAAM,WAAW,MAAM,OAAO,iBAAiB,cAAc;AAAA,QAC3D,EAAE,QAAA;AAAA,MAAQ;AAIZ,YAAM,YAAY,KAAK,IAAA,IAAQ,KAAK,KAAK;AAEzC,aAAO;AAAA,QACL,UAAU;AAAA,QACV,OAAO,SAAS;AAAA,QAChB;AAAA,QACA,QAAQ;AAAA,UACN,OAAO,WAAW;AAAA,UAClB,cAAc,WAAW;AAAA,UACzB,iBAAiB;AAAA,YACf;AAAA,YACA,cAAc,WAAW;AAAA,YACzB,UAAU,WAAW;AAAA,UAAA;AAAA,QACvB;AAAA,MACF;AAAA,IAEJ;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"token.js","sources":["../../../src/realtime/token.ts"],"sourcesContent":["import {\n createElevenLabsClient,\n getElevenLabsAgentIdFromEnv,\n} from '../utils/client'\nimport type { RealtimeToken, RealtimeTokenAdapter } from '@tanstack/ai'\nimport type { ElevenLabsRealtimeTokenOptions } from './types'\n\n/**\n * Creates an ElevenLabs realtime token adapter.\n *\n * Uses the official `@elevenlabs/elevenlabs-js` SDK to request a signed URL\n * for client-side conversation connections. The signed URL is valid for\n * 30 minutes.\n *\n * @param options - Configuration. `agentId` falls back to\n * `ELEVENLABS_AGENT_ID` in the environment when omitted.\n * @returns A RealtimeTokenAdapter for use with realtimeToken()\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'\n *\n * // Reads ELEVENLABS_AGENT_ID from env:\n * const token = await realtimeToken({ adapter: elevenlabsRealtimeToken() })\n *\n * // Or pass explicitly:\n * const token = await realtimeToken({\n * adapter: elevenlabsRealtimeToken({ agentId: 'your-agent-id' }),\n * })\n * ```\n */\nexport function elevenlabsRealtimeToken(\n options: ElevenLabsRealtimeTokenOptions = {},\n): RealtimeTokenAdapter {\n const client = createElevenLabsClient()\n\n return {\n provider: 'elevenlabs',\n\n async generateToken(): Promise<RealtimeToken> {\n const { overrides } = options\n const agentId = options.agentId ?? getElevenLabsAgentIdFromEnv()\n\n const response = await client.conversationalAi.conversations.getSignedUrl(\n { agentId },\n )\n\n // Signed URLs are valid for 30 minutes\n const expiresAt = Date.now() + 30 * 60 * 1000\n\n // Conditionally spread optional fields — the vendor target type\n // (`RealtimeSessionConfig` in `@tanstack/ai`) uses strict\n // `field?: T` semantics under `exactOptionalPropertyTypes`, so we\n // omit keys entirely when the override is absent rather than\n // setting them to `undefined`.\n return {\n provider: 'elevenlabs',\n token: response.signedUrl,\n expiresAt,\n config: {\n ...(overrides?.voiceId !== undefined && { voice: overrides.voiceId }),\n ...(overrides?.systemPrompt !== undefined && {\n instructions: overrides.systemPrompt,\n }),\n providerOptions: {\n agentId,\n ...(overrides?.firstMessage !== undefined && {\n firstMessage: overrides.firstMessage,\n }),\n ...(overrides?.language !== undefined && {\n language: overrides.language,\n }),\n },\n },\n }\n },\n }\n}\n"],"names":[],"mappings":";AAgCO,SAAS,wBACd,UAA0C,IACpB;AACtB,QAAM,SAAS,uBAAA;AAEf,SAAO;AAAA,IACL,UAAU;AAAA,IAEV,MAAM,gBAAwC;AAC5C,YAAM,EAAE,cAAc;AACtB,YAAM,UAAU,QAAQ,WAAW,4BAAA;AAEnC,YAAM,WAAW,MAAM,OAAO,iBAAiB,cAAc;AAAA,QAC3D,EAAE,QAAA;AAAA,MAAQ;AAIZ,YAAM,YAAY,KAAK,IAAA,IAAQ,KAAK,KAAK;AAOzC,aAAO;AAAA,QACL,UAAU;AAAA,QACV,OAAO,SAAS;AAAA,QAChB;AAAA,QACA,QAAQ;AAAA,UACN,GAAI,WAAW,YAAY,UAAa,EAAE,OAAO,UAAU,QAAA;AAAA,UAC3D,GAAI,WAAW,iBAAiB,UAAa;AAAA,YAC3C,cAAc,UAAU;AAAA,UAAA;AAAA,UAE1B,iBAAiB;AAAA,YACf;AAAA,YACA,GAAI,WAAW,iBAAiB,UAAa;AAAA,cAC3C,cAAc,UAAU;AAAA,YAAA;AAAA,YAE1B,GAAI,WAAW,aAAa,UAAa;AAAA,cACvC,UAAU,UAAU;AAAA,YAAA;AAAA,UACtB;AAAA,QACF;AAAA,MACF;AAAA,IAEJ;AAAA,EAAA;AAEJ;"}
@@ -39,8 +39,8 @@ function arrayBufferToBase64(buffer) {
39
39
  }
40
40
  const view = new Uint8Array(buffer);
41
41
  let binary = "";
42
- for (let i = 0; i < view.byteLength; i += 1) {
43
- binary += String.fromCharCode(view[i]);
42
+ for (const byte of view) {
43
+ binary += String.fromCharCode(byte);
44
44
  }
45
45
  return btoa(binary);
46
46
  }
@@ -100,6 +100,7 @@ function parseOutputFormat(fmt) {
100
100
  return { format: "ulaw", contentType: "audio/basic" };
101
101
  case "alaw":
102
102
  return { format: "alaw", contentType: "audio/x-alaw-basic" };
103
+ case void 0:
103
104
  default:
104
105
  return { format: "mp3", contentType: "audio/mpeg" };
105
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'\nimport {\n getApiKeyFromEnv,\n generateId as sharedGenerateId,\n} from '@tanstack/ai-utils'\nimport type { ElevenLabsOutputFormat } from '../model-meta'\n\n/**\n * Configuration for any ElevenLabs adapter. When `apiKey` is omitted we read\n * `ELEVENLABS_API_KEY` from `process.env` / `window.env` to match the\n * pattern the realtime adapters already use.\n */\nexport interface ElevenLabsClientConfig {\n apiKey?: string\n /** Override the API base URL — handy for tests + self-hosted proxies. */\n baseUrl?: string\n /** Per-request timeout passed through to the SDK (ms). */\n timeoutInSeconds?: number\n /** Override the number of SDK-level retries. */\n maxRetries?: number\n /** Extra headers attached to every request (e.g. test multiplexing). */\n headers?: Record<string, string>\n}\n\ninterface EnvObject {\n ELEVENLABS_API_KEY?: string\n ELEVENLABS_AGENT_ID?: string\n}\n\ninterface WindowWithEnv {\n env?: EnvObject\n}\n\nfunction getEnvironment(): EnvObject | undefined {\n if (typeof globalThis !== 'undefined') {\n const win = (globalThis as { window?: WindowWithEnv }).window\n if (win?.env) return win.env\n }\n if (typeof process !== 'undefined') {\n return process.env as EnvObject\n }\n return undefined\n}\n\nexport function getElevenLabsApiKeyFromEnv(): string {\n return getApiKeyFromEnv('ELEVENLABS_API_KEY')\n}\n\nexport function getElevenLabsAgentIdFromEnv(): string {\n const id = getEnvironment()?.ELEVENLABS_AGENT_ID\n if (!id) {\n throw new Error(\n 'ELEVENLABS_AGENT_ID is required. Please set it in your environment ' +\n 'variables or pass `agentId` explicitly to elevenlabsRealtimeToken().',\n )\n }\n return id\n}\n\n/**\n * Build an `ElevenLabsClient` with env-based or explicit credentials.\n * Each adapter calls this once at construction time so unit tests can\n * pass in an explicit key without needing `process.env`.\n */\nexport function createElevenLabsClient(\n config?: ElevenLabsClientConfig,\n): ElevenLabsClient {\n const apiKey = config?.apiKey ?? getElevenLabsApiKeyFromEnv()\n return new ElevenLabsClient({\n apiKey,\n ...(config?.baseUrl ? { baseUrl: config.baseUrl } : {}),\n ...(config?.timeoutInSeconds != null\n ? { timeoutInSeconds: config.timeoutInSeconds }\n : {}),\n ...(config?.maxRetries != null ? { maxRetries: config.maxRetries } : {}),\n ...(config?.headers ? { headers: config.headers } : {}),\n })\n}\n\n// Re-exported from `@tanstack/ai-utils` so existing callers keep working\n// while the implementation stays deduped across provider packages.\nexport const generateId = sharedGenerateId\n\n/**\n * Convert an ArrayBuffer to base64 in a cross-runtime way.\n *\n * The naive `btoa(String.fromCharCode(...bytes))` form blows up V8's argument\n * limit (~65k) on realistic audio payloads, so we either use `Buffer`\n * (Node / Bun) or walk the byte array in a single loop (browser).\n */\nexport function arrayBufferToBase64(buffer: ArrayBuffer): string {\n if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {\n return Buffer.from(buffer).toString('base64')\n }\n const view = new Uint8Array(buffer)\n let binary = ''\n for (let i = 0; i < view.byteLength; i += 1) {\n binary += String.fromCharCode(view[i]!)\n }\n return btoa(binary)\n}\n\n/**\n * Drain a `ReadableStream<Uint8Array>` (what the ElevenLabs SDK returns for\n * audio endpoints) into a single `Uint8Array`, then expose it as an\n * `ArrayBuffer` slice. We concatenate ourselves rather than going through\n * `new Response(stream).arrayBuffer()` so we stay runtime-agnostic.\n */\nexport async function readStreamToArrayBuffer(\n stream: ReadableStream<Uint8Array>,\n): Promise<ArrayBuffer> {\n const reader = stream.getReader()\n const chunks: Array<Uint8Array> = []\n let total = 0\n try {\n let result = await reader.read()\n while (!result.done) {\n chunks.push(result.value)\n total += result.value.byteLength\n result = await reader.read()\n }\n } finally {\n reader.releaseLock()\n }\n const merged = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n merged.set(chunk, offset)\n offset += chunk.byteLength\n }\n return merged.buffer.slice(\n merged.byteOffset,\n merged.byteOffset + merged.byteLength,\n )\n}\n\n/**\n * Decode a `data:` URL into a Blob for upload to the ElevenLabs STT API\n * (which only accepts multipart files or https URLs). Supports base64 and\n * URL-encoded payloads. Returns `undefined` for non-data-URL strings so the\n * caller can fall through to treating the input as an https URL.\n */\nexport function dataUrlToBlob(value: string): Blob | undefined {\n if (!value.startsWith('data:')) return undefined\n const commaIndex = value.indexOf(',')\n if (commaIndex === -1) return undefined\n\n const header = value.slice(5, commaIndex)\n const payload = value.slice(commaIndex + 1)\n const isBase64 = /;base64$/i.test(header)\n const mimeType = header.split(';')[0] || 'application/octet-stream'\n\n if (isBase64) {\n const binary = atob(payload)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i += 1) {\n bytes[i] = binary.charCodeAt(i)\n }\n return new Blob([bytes], { type: mimeType })\n }\n\n return new Blob([decodeURIComponent(payload)], { type: mimeType })\n}\n\n/**\n * Break an ElevenLabs `output_format` string (`mp3_44100_128`,\n * `pcm_24000`, `opus_48000_64`, `ulaw_8000`, ...) into a file extension and\n * content-type suitable for `TTSResult` / `AudioGenerationResult` consumers.\n *\n * Unknown codecs fall back to `mp3` / `audio/mpeg` because the ElevenLabs\n * default is `mp3_44100_128` — mispredicting on an exotic format is safer\n * than throwing in the adapter.\n */\nexport function parseOutputFormat(fmt: ElevenLabsOutputFormat | undefined): {\n format: string\n contentType: string\n} {\n const codec = (fmt || 'mp3_44100_128').split('_')[0]?.toLowerCase()\n switch (codec) {\n case 'mp3':\n return { format: 'mp3', contentType: 'audio/mpeg' }\n case 'pcm':\n return { format: 'pcm', contentType: 'audio/pcm' }\n case 'opus':\n return { format: 'opus', contentType: 'audio/opus' }\n case 'ulaw':\n return { format: 'ulaw', contentType: 'audio/basic' }\n case 'alaw':\n return { format: 'alaw', contentType: 'audio/x-alaw-basic' }\n default:\n return { format: 'mp3', contentType: 'audio/mpeg' }\n }\n}\n"],"names":["sharedGenerateId"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'\nimport {\n getApiKeyFromEnv,\n generateId as sharedGenerateId,\n} from '@tanstack/ai-utils'\nimport type { ElevenLabsOutputFormat } from '../model-meta'\n\n/**\n * Configuration for any ElevenLabs adapter. When `apiKey` is omitted we read\n * `ELEVENLABS_API_KEY` from `process.env` / `window.env` to match the\n * pattern the realtime adapters already use.\n */\nexport interface ElevenLabsClientConfig {\n apiKey?: string\n /** Override the API base URL — handy for tests + self-hosted proxies. */\n baseUrl?: string\n /** Per-request timeout passed through to the SDK (ms). */\n timeoutInSeconds?: number\n /** Override the number of SDK-level retries. */\n maxRetries?: number\n /** Extra headers attached to every request (e.g. test multiplexing). */\n headers?: Record<string, string>\n}\n\ninterface EnvObject {\n ELEVENLABS_API_KEY?: string\n ELEVENLABS_AGENT_ID?: string\n}\n\ninterface WindowWithEnv {\n env?: EnvObject\n}\n\nfunction getEnvironment(): EnvObject | undefined {\n if (typeof globalThis !== 'undefined') {\n const win = (globalThis as { window?: WindowWithEnv }).window\n if (win?.env) return win.env\n }\n if (typeof process !== 'undefined') {\n return process.env as EnvObject\n }\n return undefined\n}\n\nexport function getElevenLabsApiKeyFromEnv(): string {\n return getApiKeyFromEnv('ELEVENLABS_API_KEY')\n}\n\nexport function getElevenLabsAgentIdFromEnv(): string {\n const id = getEnvironment()?.ELEVENLABS_AGENT_ID\n if (!id) {\n throw new Error(\n 'ELEVENLABS_AGENT_ID is required. Please set it in your environment ' +\n 'variables or pass `agentId` explicitly to elevenlabsRealtimeToken().',\n )\n }\n return id\n}\n\n/**\n * Build an `ElevenLabsClient` with env-based or explicit credentials.\n * Each adapter calls this once at construction time so unit tests can\n * pass in an explicit key without needing `process.env`.\n */\nexport function createElevenLabsClient(\n config?: ElevenLabsClientConfig,\n): ElevenLabsClient {\n const apiKey = config?.apiKey ?? getElevenLabsApiKeyFromEnv()\n return new ElevenLabsClient({\n apiKey,\n ...(config?.baseUrl ? { baseUrl: config.baseUrl } : {}),\n ...(config?.timeoutInSeconds != null\n ? { timeoutInSeconds: config.timeoutInSeconds }\n : {}),\n ...(config?.maxRetries != null ? { maxRetries: config.maxRetries } : {}),\n ...(config?.headers ? { headers: config.headers } : {}),\n })\n}\n\n// Re-exported from `@tanstack/ai-utils` so existing callers keep working\n// while the implementation stays deduped across provider packages.\nexport const generateId = sharedGenerateId\n\n/**\n * Convert an ArrayBuffer to base64 in a cross-runtime way.\n *\n * The naive `btoa(String.fromCharCode(...bytes))` form blows up V8's argument\n * limit (~65k) on realistic audio payloads, so we either use `Buffer`\n * (Node / Bun) or walk the byte array in a single loop (browser).\n */\nexport function arrayBufferToBase64(buffer: ArrayBuffer): string {\n if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {\n return Buffer.from(buffer).toString('base64')\n }\n const view = new Uint8Array(buffer)\n let binary = ''\n for (const byte of view) {\n binary += String.fromCharCode(byte)\n }\n return btoa(binary)\n}\n\n/**\n * Drain a `ReadableStream<Uint8Array>` (what the ElevenLabs SDK returns for\n * audio endpoints) into a single `Uint8Array`, then expose it as an\n * `ArrayBuffer` slice. We concatenate ourselves rather than going through\n * `new Response(stream).arrayBuffer()` so we stay runtime-agnostic.\n */\nexport async function readStreamToArrayBuffer(\n stream: ReadableStream<Uint8Array>,\n): Promise<ArrayBuffer> {\n const reader = stream.getReader()\n const chunks: Array<Uint8Array> = []\n let total = 0\n try {\n let result = await reader.read()\n while (!result.done) {\n chunks.push(result.value)\n total += result.value.byteLength\n result = await reader.read()\n }\n } finally {\n reader.releaseLock()\n }\n const merged = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n merged.set(chunk, offset)\n offset += chunk.byteLength\n }\n return merged.buffer.slice(\n merged.byteOffset,\n merged.byteOffset + merged.byteLength,\n )\n}\n\n/**\n * Decode a `data:` URL into a Blob for upload to the ElevenLabs STT API\n * (which only accepts multipart files or https URLs). Supports base64 and\n * URL-encoded payloads. Returns `undefined` for non-data-URL strings so the\n * caller can fall through to treating the input as an https URL.\n */\nexport function dataUrlToBlob(value: string): Blob | undefined {\n if (!value.startsWith('data:')) return undefined\n const commaIndex = value.indexOf(',')\n if (commaIndex === -1) return undefined\n\n const header = value.slice(5, commaIndex)\n const payload = value.slice(commaIndex + 1)\n const isBase64 = /;base64$/i.test(header)\n const mimeType = header.split(';')[0] || 'application/octet-stream'\n\n if (isBase64) {\n const binary = atob(payload)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i += 1) {\n bytes[i] = binary.charCodeAt(i)\n }\n return new Blob([bytes], { type: mimeType })\n }\n\n return new Blob([decodeURIComponent(payload)], { type: mimeType })\n}\n\n/**\n * Break an ElevenLabs `output_format` string (`mp3_44100_128`,\n * `pcm_24000`, `opus_48000_64`, `ulaw_8000`, ...) into a file extension and\n * content-type suitable for `TTSResult` / `AudioGenerationResult` consumers.\n *\n * Unknown codecs fall back to `mp3` / `audio/mpeg` because the ElevenLabs\n * default is `mp3_44100_128` — mispredicting on an exotic format is safer\n * than throwing in the adapter.\n */\nexport function parseOutputFormat(fmt: ElevenLabsOutputFormat | undefined): {\n format: string\n contentType: string\n} {\n const codec = (fmt || 'mp3_44100_128').split('_')[0]?.toLowerCase()\n switch (codec) {\n case 'mp3':\n return { format: 'mp3', contentType: 'audio/mpeg' }\n case 'pcm':\n return { format: 'pcm', contentType: 'audio/pcm' }\n case 'opus':\n return { format: 'opus', contentType: 'audio/opus' }\n case 'ulaw':\n return { format: 'ulaw', contentType: 'audio/basic' }\n case 'alaw':\n return { format: 'alaw', contentType: 'audio/x-alaw-basic' }\n case undefined:\n default:\n // `split('_')[0]?.toLowerCase()` can be `undefined` only if `fmt` is\n // an empty string; any other unknown codec also falls back to mp3.\n return { format: 'mp3', contentType: 'audio/mpeg' }\n }\n}\n"],"names":["sharedGenerateId"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-elevenlabs",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "ElevenLabs adapter for TanStack AI realtime voice",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -43,13 +43,13 @@
43
43
  "@tanstack/ai-utils": "0.2.0"
44
44
  },
45
45
  "peerDependencies": {
46
- "@tanstack/ai": "^0.20.0",
47
- "@tanstack/ai-client": "^0.11.2"
46
+ "@tanstack/ai": "^0.21.0",
47
+ "@tanstack/ai-client": "^0.11.4"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@vitest/coverage-v8": "4.0.14",
51
- "@tanstack/ai": "0.20.0",
52
- "@tanstack/ai-client": "0.11.2"
51
+ "@tanstack/ai": "0.21.0",
52
+ "@tanstack/ai-client": "0.11.4"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "vite build",
@@ -106,7 +106,7 @@ export class ElevenLabsAudioAdapter<
106
106
  > extends BaseAudioAdapter<TModel, ElevenLabsAudioProviderOptions> {
107
107
  readonly name = 'elevenlabs' as const
108
108
 
109
- private client: ElevenLabsClient
109
+ private readonly client: ElevenLabsClient
110
110
 
111
111
  constructor(model: TModel, config?: ElevenLabsClientConfig) {
112
112
  super(model, config ?? {})
@@ -82,7 +82,7 @@ export class ElevenLabsSpeechAdapter<
82
82
  > extends BaseTTSAdapter<TModel, ElevenLabsSpeechProviderOptions> {
83
83
  readonly name = 'elevenlabs' as const
84
84
 
85
- private client: ElevenLabsClient
85
+ private readonly client: ElevenLabsClient
86
86
 
87
87
  constructor(model: TModel, config?: ElevenLabsClientConfig) {
88
88
  super(model, config ?? {})
@@ -211,6 +211,9 @@ function inferOutputFormatFromResponseFormat(
211
211
  return 'opus_48000_128'
212
212
  case undefined:
213
213
  return undefined
214
+ case 'aac':
215
+ case 'flac':
216
+ case 'wav':
214
217
  default:
215
218
  // `aac` / `flac` / `wav` are not native ElevenLabs formats —
216
219
  // fall back to mp3 rather than blowing up mid-request.
@@ -82,7 +82,7 @@ export class ElevenLabsTranscriptionAdapter<
82
82
  > {
83
83
  readonly name = 'elevenlabs' as const
84
84
 
85
- private client: ElevenLabsClient
85
+ private readonly client: ElevenLabsClient
86
86
 
87
87
  constructor(model: TModel, config?: ElevenLabsClientConfig) {
88
88
  super(model, config ?? {})
@@ -165,6 +165,7 @@ export class ElevenLabsTranscriptionAdapter<
165
165
  // The SDK types this as a union of single- and multi-channel responses.
166
166
  // We treat multi-channel as "join the channel transcripts" — consumers
167
167
  // who care about per-channel detail can re-parse from `modelOptions`.
168
+ // eslint-disable-next-line no-restricted-syntax -- bridges SpeechToTextConvertResponse union (incl. webhook variant with no text/words/transcripts) to a flattened duck-typed shape we discriminate at runtime
168
169
  const data = response as unknown as {
169
170
  text?: string
170
171
  languageCode?: string
@@ -261,7 +262,7 @@ function buildWordsAndSegments(
261
262
  segments?: Array<TranscriptionSegment>
262
263
  } {
263
264
  const timedWords = words.filter(
264
- (w) =>
265
+ (w): w is typeof w & { start: number; end: number } =>
265
266
  typeof w.start === 'number' &&
266
267
  typeof w.end === 'number' &&
267
268
  w.type !== 'spacing',
@@ -270,8 +271,8 @@ function buildWordsAndSegments(
270
271
 
271
272
  const outWords: Array<TranscriptionWord> = timedWords.map((w) => ({
272
273
  word: w.text,
273
- start: w.start!,
274
- end: w.end!,
274
+ start: w.start,
275
+ end: w.end,
275
276
  }))
276
277
 
277
278
  // Group contiguous words that share a speaker into segments. If no speaker
@@ -287,8 +288,8 @@ function buildWordsAndSegments(
287
288
  for (const w of timedWords) {
288
289
  if (!current) {
289
290
  current = {
290
- start: w.start!,
291
- end: w.end!,
291
+ start: w.start,
292
+ end: w.end,
292
293
  text: w.text,
293
294
  ...(w.speakerId ? { speaker: w.speakerId } : {}),
294
295
  }
@@ -297,14 +298,14 @@ function buildWordsAndSegments(
297
298
  if (w.speakerId && current.speaker !== w.speakerId) {
298
299
  segments.push({ id: segments.length, ...current })
299
300
  current = {
300
- start: w.start!,
301
- end: w.end!,
301
+ start: w.start,
302
+ end: w.end,
302
303
  text: w.text,
303
304
  speaker: w.speakerId,
304
305
  }
305
306
  continue
306
307
  }
307
- current.end = w.end!
308
+ current.end = w.end
308
309
  current.text = current.text ? `${current.text} ${w.text}` : w.text
309
310
  }
310
311
  if (current) segments.push({ id: segments.length, ...current })
@@ -8,7 +8,6 @@ import type {
8
8
  RealtimeMessage,
9
9
  RealtimeMode,
10
10
  RealtimeSessionConfig,
11
- RealtimeStatus,
12
11
  RealtimeToken,
13
12
  } from '@tanstack/ai'
14
13
  import type { InternalLogger } from '@tanstack/ai/adapter-internals'
@@ -119,7 +118,7 @@ async function createElevenLabsConnection(
119
118
  logger.provider(`provider=elevenlabs direction=in type=connect`, {
120
119
  frame: { type: 'connect' },
121
120
  })
122
- emit('status_change', { status: 'connected' as RealtimeStatus })
121
+ emit('status_change', { status: 'connected' })
123
122
  emit('mode_change', { mode: 'listening' })
124
123
  },
125
124
 
@@ -127,7 +126,7 @@ async function createElevenLabsConnection(
127
126
  logger.provider(`provider=elevenlabs direction=in type=disconnect`, {
128
127
  frame: { type: 'disconnect' },
129
128
  })
130
- emit('status_change', { status: 'idle' as RealtimeStatus })
129
+ emit('status_change', { status: 'idle' })
131
130
  emit('mode_change', { mode: 'idle' })
132
131
  },
133
132
 
@@ -205,7 +204,7 @@ async function createElevenLabsConnection(
205
204
  await conversation.endSession()
206
205
  conversation = null
207
206
  }
208
- emit('status_change', { status: 'idle' as RealtimeStatus })
207
+ emit('status_change', { status: 'idle' })
209
208
  },
210
209
 
211
210
  async startAudioCapture() {
@@ -257,10 +256,12 @@ async function createElevenLabsConnection(
257
256
  event: TEvent,
258
257
  handler: RealtimeEventHandler<TEvent>,
259
258
  ): () => void {
260
- if (!eventHandlers.has(event)) {
261
- eventHandlers.set(event, new Set())
259
+ let handlers = eventHandlers.get(event)
260
+ if (!handlers) {
261
+ handlers = new Set()
262
+ eventHandlers.set(event, handlers)
262
263
  }
263
- eventHandlers.get(event)!.add(handler)
264
+ handlers.add(handler)
264
265
 
265
266
  return () => {
266
267
  eventHandlers.get(event)?.delete(handler)
@@ -49,17 +49,28 @@ export function elevenlabsRealtimeToken(
49
49
  // Signed URLs are valid for 30 minutes
50
50
  const expiresAt = Date.now() + 30 * 60 * 1000
51
51
 
52
+ // Conditionally spread optional fields — the vendor target type
53
+ // (`RealtimeSessionConfig` in `@tanstack/ai`) uses strict
54
+ // `field?: T` semantics under `exactOptionalPropertyTypes`, so we
55
+ // omit keys entirely when the override is absent rather than
56
+ // setting them to `undefined`.
52
57
  return {
53
58
  provider: 'elevenlabs',
54
59
  token: response.signedUrl,
55
60
  expiresAt,
56
61
  config: {
57
- voice: overrides?.voiceId,
58
- instructions: overrides?.systemPrompt,
62
+ ...(overrides?.voiceId !== undefined && { voice: overrides.voiceId }),
63
+ ...(overrides?.systemPrompt !== undefined && {
64
+ instructions: overrides.systemPrompt,
65
+ }),
59
66
  providerOptions: {
60
67
  agentId,
61
- firstMessage: overrides?.firstMessage,
62
- language: overrides?.language,
68
+ ...(overrides?.firstMessage !== undefined && {
69
+ firstMessage: overrides.firstMessage,
70
+ }),
71
+ ...(overrides?.language !== undefined && {
72
+ language: overrides.language,
73
+ }),
63
74
  },
64
75
  },
65
76
  }
@@ -94,8 +94,8 @@ export function arrayBufferToBase64(buffer: ArrayBuffer): string {
94
94
  }
95
95
  const view = new Uint8Array(buffer)
96
96
  let binary = ''
97
- for (let i = 0; i < view.byteLength; i += 1) {
98
- binary += String.fromCharCode(view[i]!)
97
+ for (const byte of view) {
98
+ binary += String.fromCharCode(byte)
99
99
  }
100
100
  return btoa(binary)
101
101
  }
@@ -187,7 +187,10 @@ export function parseOutputFormat(fmt: ElevenLabsOutputFormat | undefined): {
187
187
  return { format: 'ulaw', contentType: 'audio/basic' }
188
188
  case 'alaw':
189
189
  return { format: 'alaw', contentType: 'audio/x-alaw-basic' }
190
+ case undefined:
190
191
  default:
192
+ // `split('_')[0]?.toLowerCase()` can be `undefined` only if `fmt` is
193
+ // an empty string; any other unknown codec also falls back to mp3.
191
194
  return { format: 'mp3', contentType: 'audio/mpeg' }
192
195
  }
193
196
  }