Ctrl K
ComunidadGuías…

tts_providers/groq_provider.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
Groq Orpheus TTS Provider — canopylabs/orpheus-v1-english via Groq LPU.

~130-200ms TTFB, natural human-like prosody, MP3 output.
API key: GROQ_API_KEY env var
"""

import os
import time
import logging

from .base_provider import TTSProvider

logger = logging.getLogger(__name__)

MODEL = "canopylabs/orpheus-v1-english"
# If a single TTS call exceeds this, abort and let fallback handle it.
# Normal Groq latency is 130-700ms. Rate-limited calls take 12-27s.
GROQ_TTS_TIMEOUT_SECONDS = 5.0

AVAILABLE_VOICES = [
    "autumn",  # Female (default)
    "diana",   # Female
    "hannah",  # Female
    "austin",  # Male
    "daniel",  # Male
    "troy",    # Male
]


class GroqProvider(TTSProvider):
    """
    TTS Provider using Groq Orpheus (canopylabs/orpheus-v1-english).

    Voices: autumn, diana, hannah, austin, daniel, troy
    Output: WAV audio bytes
    Latency: ~130-200ms (Groq LPU)
    Cost: ~$0.05/1K chars
    """

    def __init__(self):
        super().__init__()
        self.api_key = os.getenv('GROQ_API_KEY', '')
        self._status = 'active' if self.api_key else 'error'
        self._init_error = None if self.api_key else 'GROQ_API_KEY not set'
        self._client = None

    def _get_client(self):
        if self._client is None:
            try:
                from groq import Groq
                self._client = Groq(
                    api_key=self.api_key,
                    timeout=GROQ_TTS_TIMEOUT_SECONDS,
                )
                # JamBot Books: record every Groq call on this client (TTS success +
                # failures) via the httpx response hook. This is the LIVE voice path
                # (get_provider('groq')); without it nothing recorded → no fire to
                # prov:groq on the board. Mirrors services/tts.py get_groq_client().
                try:
                    from services.jambot_books_hook import attach_groq
                    attach_groq(self._client)
                except Exception:
                    pass
            except ImportError:
                raise RuntimeError("groq package not installed — run: pip install groq")
        return self._client

    def generate_speech(self, text: str, voice: str = 'autumn', **kwargs) -> bytes:
        """
        Generate speech via Groq Orpheus.

        Args:
            text: Text to synthesize.
            voice: One of AVAILABLE_VOICES. Default: 'autumn'.

        Returns:
            MP3 audio bytes.
        """
        if not self.api_key:
            raise RuntimeError("GROQ_API_KEY not set")

        self.validate_text(text)

        if voice not in AVAILABLE_VOICES:
            logger.warning(f"[Groq] Unknown voice '{voice}', using autumn")
            voice = 'autumn'

        t = time.time()
        logger.info(f"[Groq] Requesting TTS: '{text[:60]}' voice={voice}")

        try:
            client = self._get_client()
            resp = client.audio.speech.create(
                model=MODEL,
                voice=voice,
                input=text,
                response_format="wav",
            )
            audio_bytes = resp.content if hasattr(resp, 'content') else resp.read()
        except Exception as e:
            # Parse structured Groq API errors to extract error code
            import re
            err_str = str(e)
            err_code = 'unknown'
            err_msg = err_str
            try:
                code_match = re.search(r"'code':\s*'([^']+)'", err_str)
                msg_match = re.search(r"'message':\s*'([^']+)'", err_str)
                if code_match:
                    err_code = code_match.group(1)
                if msg_match:
                    err_msg = msg_match.group(1)
            except Exception:
                pass
            raise RuntimeError(f"[groq:{err_code}] {err_msg}")

        elapsed = int((time.time() - t) * 1000)
        logger.info(f"[Groq] Generated {len(audio_bytes)} bytes in {elapsed}ms")
        return audio_bytes

    def health_check(self) -> dict:
        if not self.api_key:
            return {"ok": False, "latency_ms": 0, "detail": "GROQ_API_KEY not set"}
        t = time.time()
        try:
            from groq import Groq
            client = Groq(api_key=self.api_key)
            client.models.list()
            latency_ms = int((time.time() - t) * 1000)
            return {"ok": True, "latency_ms": latency_ms, "detail": "Groq reachable — Orpheus ready"}
        except Exception as e:
            latency_ms = int((time.time() - t) * 1000)
            return {"ok": False, "latency_ms": latency_ms, "detail": str(e)}

    def list_voices(self) -> list:
        return AVAILABLE_VOICES.copy()

    def get_default_voice(self) -> str:
        return 'autumn'

    def is_available(self) -> bool:
        return bool(self.api_key)

    def get_info(self) -> dict:
        return {
            'name': 'Groq Orpheus',
            'provider_id': 'groq',
            'status': self._status,
            'description': 'Orpheus TTS via Groq LPU — fast, natural, human-like prosody',
            'quality': 'high',
            'latency': 'very-fast',
            'cost_per_minute': 0.05,
            'voices': AVAILABLE_VOICES.copy(),
            'features': ['fast', 'natural', 'empathetic', 'mp3-output', 'cloud'],
            'requires_api_key': True,
            'languages': ['en'],
            'max_characters': 5000,
            'notes': 'Orpheus v1 English on Groq LPU. ~130-200ms latency. GROQ_API_KEY required.',
            'default_voice': 'autumn',
            'audio_format': 'wav',
            'sample_rate': 24000,
            'error': self._init_error,
        }