package provider import ( "context" "encoding/json" "fmt" "net/http" "strings" ) // ElevenLabs constants (public contract: the env names are part of the // fleet documentation, the endpoint default part of the configurability). const ( // DefaultEndpointElevenLabs is the production API endpoint. DefaultEndpointElevenLabs = "https://api.elevenlabs.io" // EnvElevenLabsKey provides the API key (spec: keys come exclusively // from the environment, never from config.toml). EnvElevenLabsKey = "TALK_ELEVENLABS_KEY" // EnvElevenLabsEndpoint overrides the endpoint (tests/corporate // servers; beats config.toml, see the voices.go/chain.go docs). EnvElevenLabsEndpoint = "TALK_ELEVENLABS_ENDPOINT" elevenLabsModel = "eleven_multilingual_v2" // multilingual v2 ) // ElevenLabs is the premium tier of the chain. Synthesize performs POST // /v1/text-to-speech/ with header `xi-api-key` and body // {text, model_id: "eleven_multilingual_v2", voice_settings: // {stability: 0.5, similarity_boost: 0.75}} and returns the response bytes // unchanged. Return format: MP3 (documented contract of the audio package). type ElevenLabs struct { // Endpoint is the base URL (without path); empty is replaced by the // default at construction. Endpoint string // APIKey is the ElevenLabs key (from TALK_ELEVENLABS_KEY). APIKey string // Voices is the config mapping voice.elevenlabs. = id; empty = // neutral repo defaults (see voices.go). Voices map[string]string // Client is the HTTP client; nil = shared client with timeout // (cloudTimeout). Client *http.Client pinned string // --voice pin (VoicePinner) } // NewElevenLabs builds the provider from the chain configuration: endpoint // from TALK_ELEVENLABS_ENDPOINT > [providers.elevenlabs].endpoint > default; // key exclusively from TALK_ELEVENLABS_KEY. func NewElevenLabs(cfg ChainConfig) *ElevenLabs { return &ElevenLabs{ Endpoint: endpointFrom(cfg, "elevenlabs", EnvElevenLabsEndpoint, DefaultEndpointElevenLabs), APIKey: cfg.Getenv(EnvElevenLabsKey), Voices: cfg.Voices["elevenlabs"], } } // Name implements Provider. func (*ElevenLabs) Name() string { return "elevenlabs" } // PinVoice implements VoicePinner (CLI flag --voice). func (p *ElevenLabs) PinVoice(id string) { p.pinned = id } // Available implements Provider: key and endpoint must be set (deliberately // without a network call; Available must stay cheap. Whether the key is // valid is determined by Synthesize, and the chain falls back). func (p *ElevenLabs) Available(ctx context.Context) bool { return p.APIKey != "" && p.Endpoint != "" } // elevenLabsRequest is the request body (literal values from the API // contract). type elevenLabsRequest struct { Text string `json:"text"` ModelID string `json:"model_id"` VoiceSettings elevenLabsVoiceSettings `json:"voice_settings"` } type elevenLabsVoiceSettings struct { Stability float64 `json:"stability"` SimilarityBoost float64 `json:"similarity_boost"` } // Synthesize implements Provider. Return format: MP3. func (p *ElevenLabs) Synthesize(ctx context.Context, text, lang string) ([]byte, error) { if text == "" { return nil, fmt.Errorf("elevenlabs: empty text") } voice := resolveVoice("elevenlabs", lang, p.pinned, p.Voices) if voice == "" { return nil, fmt.Errorf("elevenlabs: no voice configured for language %q", lang) } body, err := json.Marshal(elevenLabsRequest{ Text: text, ModelID: elevenLabsModel, VoiceSettings: elevenLabsVoiceSettings{Stability: 0.5, SimilarityBoost: 0.75}, }) if err != nil { return nil, fmt.Errorf("elevenlabs: building request body: %w", err) } url := strings.TrimRight(p.Endpoint, "/") + "/v1/text-to-speech/" + voice return postForBytes(ctx, p.Client, "elevenlabs", url, map[string]string{ "xi-api-key": p.APIKey, "Accept": "audio/mpeg", }, body) }