Files

79 lines
2.6 KiB
Python

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)}