codeAtlas / src /integrations /elevenlabs.py
aghilsabu's picture
feat: add ElevenLabs voice synthesis integration
c0c9f39
"""ElevenLabs Voice Integration"""
import logging
from datetime import datetime
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple
from ..config import get_config, AUDIOS_DIR
logger = logging.getLogger("codeatlas.elevenlabs")
try:
from elevenlabs.client import ElevenLabs
ELEVENLABS_AVAILABLE = True
except ImportError:
ELEVENLABS_AVAILABLE = False
@dataclass
class VoiceConfig:
voice_id: str = "JBFqnCBsd6RMkjVDRZzb"
model_id: str = "eleven_multilingual_v2"
output_format: str = "mp3_44100_128"
AVAILABLE_VOICES = {
"George (Male)": "JBFqnCBsd6RMkjVDRZzb",
"Rachel (Female)": "21m00Tcm4TlvDq8ikWAM",
"Adam (Male)": "pNInz6obpgDQGcFmaJgB",
"Bella (Female)": "EXAVITQu4vr4xnSDxMaL",
"Antoni (Male)": "ErXwobaYiN019PkySvjV",
}
class VoiceNarrator:
def __init__(self, api_key: Optional[str] = None, voice_config: Optional[VoiceConfig] = None):
self.config = get_config()
self.api_key = api_key or self.config.elevenlabs_api_key
self.voice_config = voice_config or VoiceConfig()
self.audios_dir = AUDIOS_DIR
self._client = None
@property
def available(self) -> bool:
return ELEVENLABS_AVAILABLE and bool(self.api_key)
@property
def client(self):
if self._client is None and self.available:
self._client = ElevenLabs(api_key=self.api_key)
return self._client
def generate(self, text: str, voice_id: Optional[str] = None) -> Tuple[Optional[Path], Optional[str]]:
if not ELEVENLABS_AVAILABLE:
return None, "ElevenLabs not installed. Run: pip install elevenlabs"
if not self.api_key:
return None, "ElevenLabs API key not configured"
if not text or not text.strip():
return None, "No text provided"
try:
audio = self.client.text_to_speech.convert(
text=text,
voice_id=voice_id or self.voice_config.voice_id,
model_id=self.voice_config.model_id,
output_format=self.voice_config.output_format,
)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
audio_path = self.audios_dir / f"summary_{timestamp}.mp3"
with open(audio_path, "wb") as f:
for chunk in audio:
f.write(chunk)
logger.info(f"Generated audio: {audio_path}")
return audio_path, None
except Exception as e:
logger.exception("Audio generation failed")
return None, f"Error: {str(e)}"
def get_voices(self) -> dict:
return AVAILABLE_VOICES