package academy import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "time" ) // ElevenLabsClient handles text-to-speech via the ElevenLabs API type ElevenLabsClient struct { apiKey string voiceID string client *http.Client } // NewElevenLabsClient creates a new ElevenLabs client func NewElevenLabsClient() *ElevenLabsClient { apiKey := os.Getenv("ELEVENLABS_API_KEY") voiceID := os.Getenv("ELEVENLABS_VOICE_ID") if voiceID == "" { voiceID = "EXAVITQu4vr4xnSDxMaL" // Default: "Sarah" voice } return &ElevenLabsClient{ apiKey: apiKey, voiceID: voiceID, client: &http.Client{ Timeout: 120 * time.Second, }, } } // IsConfigured returns true if API key is set func (c *ElevenLabsClient) IsConfigured() bool { return c.apiKey != "" } // TextToSpeechRequest represents the API request type TextToSpeechRequest struct { Text string `json:"text"` ModelID string `json:"model_id"` VoiceSettings VoiceSettings `json:"voice_settings"` } // VoiceSettings controls voice parameters type VoiceSettings struct { Stability float64 `json:"stability"` SimilarityBoost float64 `json:"similarity_boost"` Style float64 `json:"style"` } // TextToSpeech converts text to speech audio (MP3) func (c *ElevenLabsClient) TextToSpeech(text string) ([]byte, error) { if !c.IsConfigured() { return nil, fmt.Errorf("ElevenLabs API key not configured") } url := fmt.Sprintf("https://api.elevenlabs.io/v1/text-to-speech/%s", c.voiceID) reqBody := TextToSpeechRequest{ Text: text, ModelID: "eleven_multilingual_v2", VoiceSettings: VoiceSettings{ Stability: 0.5, SimilarityBoost: 0.75, Style: 0.5, }, } jsonBody, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } req, err := http.NewRequest("POST", url, bytes.NewReader(jsonBody)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("xi-api-key", c.apiKey) req.Header.Set("Accept", "audio/mpeg") resp, err := c.client.Do(req) if err != nil { return nil, fmt.Errorf("ElevenLabs API request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("ElevenLabs API error %d: %s", resp.StatusCode, string(body)) } audioData, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read audio response: %w", err) } return audioData, nil }