"""AWS Polly text-to-speech service.""" import logging import time _logger = logging.getLogger(__name__) try: import boto3 as _boto3 except ImportError: _boto3 = None VOICE_MAP = { "en-GB": {"female": "Amy", "male": "Brian"}, "en-US": {"female": "Joanna", "male": "Matthew"}, "en-AU": {"female": "Nicole", "male": "Russell"}, "en-IN": {"female": "Aditi", "male": "Aditi"}, } class PollyService: """AWS Polly TTS for generating listening exam audio.""" def __init__(self, env): self.env = env self._get_param = env["ir.config_parameter"].sudo().get_param self._client = None def _get_client(self): if self._client: return self._client if not _boto3: raise RuntimeError("boto3 not installed — run: pip install boto3") access_key = self._get_param("encoach_ai.aws_access_key", "") secret_key = self._get_param("encoach_ai.aws_secret_key", "") region = self._get_param("encoach_ai.aws_region", "eu-west-1") if not access_key or not secret_key: import os access_key = access_key or os.environ.get("AWS_ACCESS_KEY_ID", "") secret_key = secret_key or os.environ.get("AWS_SECRET_ACCESS_KEY", "") if not access_key: raise RuntimeError("AWS credentials not configured — set in AI Settings") self._client = _boto3.client( "polly", aws_access_key_id=access_key, aws_secret_access_key=secret_key, region_name=region, ) return self._client def _log(self, action, latency, status="success", error=None): try: self.env["encoach.ai.log"].sudo().create({ "service": "polly", "action": action, "latency_ms": latency, "status": status, "error_message": error, }) except Exception: pass def synthesize(self, text, *, voice=None, language="en-GB", gender="female", engine="neural", output_format="mp3"): """Convert text to speech audio bytes. Returns: dict with 'audio' (bytes), 'content_type', 'voice', 'characters' """ client = self._get_client() if not voice: voice = VOICE_MAP.get(language, VOICE_MAP["en-GB"]).get(gender, "Amy") t0 = time.time() try: resp = client.synthesize_speech( Text=text, OutputFormat=output_format, VoiceId=voice, Engine=engine, LanguageCode=language, ) audio = resp["AudioStream"].read() latency = int((time.time() - t0) * 1000) self._log("synthesize", latency) return { "audio": audio, "content_type": resp["ContentType"], "voice": voice, "characters": len(text), } except Exception as exc: self._log("synthesize", int((time.time() - t0) * 1000), "error", str(exc)) raise def list_voices(self, language="en-GB"): """List available voices for a language.""" client = self._get_client() resp = client.describe_voices(LanguageCode=language) return [ {"id": v["Id"], "name": v["Name"], "gender": v["Gender"], "engine": v.get("SupportedEngines", [])} for v in resp.get("Voices", []) ]