@@ -4,6 +4,8 @@ import type {
44 SpeechVoice ,
55 TextToSpeechProvider ,
66} from '../types.js' ;
7+ import { ApiKeyPool } from '../../core/providers/ApiKeyPool.js' ;
8+ import { isQuotaError } from '../../core/providers/quotaErrors.js' ;
79
810/**
911 * Configuration for the {@link ElevenLabsTextToSpeechProvider}.
@@ -108,6 +110,9 @@ export class ElevenLabsTextToSpeechProvider implements TextToSpeechProvider {
108110 /** Fetch implementation — injected for testability, defaults to global fetch. */
109111 private readonly fetchImpl : typeof fetch ;
110112
113+ /** API key pool for round-robin rotation and quota failover. */
114+ private readonly keyPool : ApiKeyPool ;
115+
111116 /**
112117 * Creates a new ElevenLabsTextToSpeechProvider.
113118 *
@@ -124,6 +129,7 @@ export class ElevenLabsTextToSpeechProvider implements TextToSpeechProvider {
124129 */
125130 constructor ( private readonly config : ElevenLabsTextToSpeechProviderConfig ) {
126131 this . fetchImpl = config . fetchImpl ?? fetch ;
132+ this . keyPool = new ApiKeyPool ( config . apiKey ) ;
127133 }
128134
129135 /**
@@ -180,45 +186,53 @@ export class ElevenLabsTextToSpeechProvider implements TextToSpeechProvider {
180186
181187 const model = options . model ?? this . config . model ?? 'eleven_multilingual_v2' ;
182188
183- const response = await this . fetchImpl (
184- `${ this . config . baseUrl ?? 'https://api.elevenlabs.io/v1' } /text-to-speech/${ voiceId } ` ,
185- {
189+ const baseUrl = this . config . baseUrl ?? 'https://api.elevenlabs.io/v1' ;
190+ const requestBody = JSON . stringify ( {
191+ text,
192+ model_id : model ,
193+ voice_settings : {
194+ stability :
195+ typeof options . providerSpecificOptions ?. stability === 'number'
196+ ? options . providerSpecificOptions . stability
197+ : 0.5 ,
198+ similarity_boost :
199+ typeof options . providerSpecificOptions ?. similarityBoost === 'number'
200+ ? options . providerSpecificOptions . similarityBoost
201+ : 0.75 ,
202+ style :
203+ typeof options . providerSpecificOptions ?. style === 'number'
204+ ? options . providerSpecificOptions . style
205+ : undefined ,
206+ use_speaker_boost :
207+ typeof options . providerSpecificOptions ?. useSpeakerBoost === 'boolean'
208+ ? options . providerSpecificOptions . useSpeakerBoost
209+ : true ,
210+ } ,
211+ } ) ;
212+
213+ const doFetch = ( key : string ) =>
214+ this . fetchImpl ( `${ baseUrl } /text-to-speech/${ voiceId } ` , {
186215 method : 'POST' ,
187216 headers : {
188- // ElevenLabs uses its own header format instead of standard Authorization
189- 'xi-api-key' : this . config . apiKey ,
217+ 'xi-api-key' : key ,
190218 'Content-Type' : 'application/json' ,
191- // Request MP3 format in the response
192219 Accept : 'audio/mpeg' ,
193220 } ,
194- body : JSON . stringify ( {
195- text,
196- model_id : model ,
197- voice_settings : {
198- // Extract provider-specific settings with sensible defaults.
199- // These defaults produce natural-sounding output for most voices.
200- stability :
201- typeof options . providerSpecificOptions ?. stability === 'number'
202- ? options . providerSpecificOptions . stability
203- : 0.5 ,
204- similarity_boost :
205- typeof options . providerSpecificOptions ?. similarityBoost === 'number'
206- ? options . providerSpecificOptions . similarityBoost
207- : 0.75 ,
208- // Style is only meaningful for v2+ models; omit if not specified
209- style :
210- typeof options . providerSpecificOptions ?. style === 'number'
211- ? options . providerSpecificOptions . style
212- : undefined ,
213- // Speaker boost enhances vocal clarity and similarity
214- use_speaker_boost :
215- typeof options . providerSpecificOptions ?. useSpeakerBoost === 'boolean'
216- ? options . providerSpecificOptions . useSpeakerBoost
217- : true ,
218- } ,
219- } ) ,
221+ body : requestBody ,
222+ } ) ;
223+
224+ const key = this . keyPool . next ( ) ;
225+ let response = await doFetch ( key ) ;
226+
227+ if ( ! response . ok && this . keyPool . size > 1 ) {
228+ const errBody = await response . text ( ) . catch ( ( ) => '' ) ;
229+ if ( isQuotaError ( response . status , errBody ) ) {
230+ this . keyPool . markExhausted ( key ) ;
231+ response = await doFetch ( this . keyPool . next ( ) ) ;
232+ } else {
233+ throw new Error ( `ElevenLabs synthesis failed (${ response . status } ): ${ errBody } ` ) ;
220234 }
221- ) ;
235+ }
222236
223237 if ( ! response . ok ) {
224238 const message = await response . text ( ) ;
0 commit comments