import logging, subprocess from typing import Tuple, Any, Dict, List import config logger = logging.getLogger(__name__) def _run_ps(commands, timeout=60): script = "; ".join(commands) try: p = subprocess.run( ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script], capture_output=True, text=True, timeout=timeout) return p.returncode, p.stdout.strip() except subprocess.TimeoutExpired: return -1, "timeout" except Exception as e: return -1, str(e) def list_voices() -> List[Dict[str, str]]: cmds = [ "Add-Type -AssemblyName System.Speech", "$s = New-Object System.Speech.Synthesis.SpeechSynthesizer", "foreach ($v in $s.GetInstalledVoices()) {", " $i = $v.VoiceInfo", ' Write-Host ("VOICE:" + $i.Name + "|" + $i.Description + "|" + $i.Culture + "|" + $i.Gender + "|" + $i.Age)', "}", "$s.Dispose()", ] code, out = _run_ps(cmds) result = [] for line in out.splitlines(): if line.startswith("VOICE:"): parts = line[6:].strip().split("|") if len(parts) >= 5: result.append({"name": parts[0].strip(), "description": parts[1].strip(), "culture": parts[2].strip(), "gender": parts[3].strip(), "age": parts[4].strip()}) return result def speak(text: str) -> Tuple[bool, Dict[str, Any]]: if not config.TTS_ENABLED: logger.info("TTS disabled, skipping: %s", text) return True, {"skipped": True} text = text[:config.TTS_MAX_TEXT_LENGTH].strip() if not text: return False, {"error": "empty text after truncation"} safe = text.replace(chr(34), chr(34) + chr(34)) vname = (config.TTS_VOICE_NAME or "").replace(chr(34), chr(34) + chr(34)) cmds = [ "Add-Type -AssemblyName System.Speech", "$s = New-Object System.Speech.Synthesis.SpeechSynthesizer", ] if vname: cmds += [ "foreach ($v in $s.GetInstalledVoices()) {", ' if ($v.VoiceInfo.Name -like "*' + vname + '*") { $s.SelectVoice($v.VoiceInfo.Name); break }', "}", ] cmds += [ "$s.Rate = " + str(config.TTS_RATE), "$s.Volume = 100", '$s.Speak("' + safe + '")', "$s.Dispose()", ] try: code, out = _run_ps(cmds) if code != 0: return False, {"error": f"TTS failed: {out}"} return True, {"spoken": True} except Exception as e: logger.exception("TTS failed") return False, {"error": str(e)}