package provider import ( "context" "encoding/json" "fmt" "net/http" "strings" ) // ElevenLabs-Konstanten (öffentlicher Vertrag: Env-Namen sind Teil der // Fleet-Doku, Endpunkt-Default Teil der Konfigurierbarkeit). const ( // DefaultEndpointElevenLabs ist der produktive API-Endpunkt. DefaultEndpointElevenLabs = "https://api.elevenlabs.io" // EnvElevenLabsKey liefert den API-Key (Spec: Keys ausschließlich Env, // nie config.toml). EnvElevenLabsKey = "TALK_ELEVENLABS_KEY" // EnvElevenLabsEndpoint überschreibt den Endpunkt (Tests/Firmenserver; // schlägt die config.toml, siehe voices.go/chain.go-Doku). EnvElevenLabsEndpoint = "TALK_ELEVENLABS_ENDPOINT" elevenLabsModel = "eleven_multilingual_v2" // multilingual v2, Task-Brief ) // ElevenLabs ist die Premium-Stufe der Kette. Synthesize führt POST // /v1/text-to-speech/ aus — Header `xi-api-key`, Body // {text, model_id: "eleven_multilingual_v2", voice_settings: // {stability: 0.5, similarity_boost: 0.75}} — und liefert die Antwort-Bytes // unverändert zurück. Rückgabeformat: MP3 (dokumentierter Vertrag des // audio-Pakets). type ElevenLabs struct { // Endpoint ist die Basis-URL (ohne Pfad); leer wird beim Bau durch den // Default ersetzt. Endpoint string // APIKey ist der ElevenLabs-Key (aus TALK_ELEVENLABS_KEY). APIKey string // Voices ist das config-Mapping voice.elevenlabs. = id; leer = // neutrale Repo-Defaults (siehe voices.go). Voices map[string]string // Client ist der HTTP-Client; nil = geteilter Client mit Timeout // (cloudTimeout). Client *http.Client pinned string // --voice-Pin (VoicePinner) } // NewElevenLabs baut den Provider aus der Ketten-Konfiguration: Endpunkt // aus TALK_ELEVENLABS_ENDPOINT > [providers.elevenlabs].endpoint > Default; // Key ausschließlich aus 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 implementiert Provider. func (*ElevenLabs) Name() string { return "elevenlabs" } // PinVoice implementiert VoicePinner (CLI-Flag --voice). func (p *ElevenLabs) PinVoice(id string) { p.pinned = id } // Available implementiert Provider: Key und Endpunkt müssen gesetzt sein // (bewusst ohne Netz-Call — Available muss billig bleiben; ob der Key // gültig ist, stellt Synthesize fest und die Kette fällt weiter). func (p *ElevenLabs) Available(ctx context.Context) bool { return p.APIKey != "" && p.Endpoint != "" } // elevenLabsRequest ist der Antwort-Body (Task-Brief, wörtliche Werte). 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 implementiert Provider. Rückgabeformat: MP3. func (p *ElevenLabs) Synthesize(ctx context.Context, text, lang string) ([]byte, error) { if text == "" { return nil, fmt.Errorf("elevenlabs: leerer Text") } voice := resolveVoice("elevenlabs", lang, p.pinned, p.Voices) if voice == "" { return nil, fmt.Errorf("elevenlabs: keine Stimme für Sprache %q konfiguriert", 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: Anfrage-Body bauen: %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) }