"""ChatService, DocumentService, TTSService.""" import asyncio import platform import subprocess import tempfile from typing import AsyncIterator from api_client import APIClientProtocol from config import AppConfig class ChatService: def __init__(self, api_client: APIClientProtocol): self._api = api_client def chat( self, message: str, user_id: str, show_thinking: bool ) -> AsyncIterator[tuple[str, str | None]]: return self._api.chat(message, user_id, show_thinking) async def reset(self, user_id: str) -> None: await self._api.reset(user_id) async def save_feedback( self, user_id: str, user_msg: str, asst_msg: str, rating: int, run_id: str | None, ) -> None: await self._api.save_feedback(user_id, user_msg, asst_msg, rating, run_id) class DocumentService: def __init__(self, api_client: APIClientProtocol): self._api = api_client async def ingest(self, file_path: str) -> dict: return await self._api.ingest(file_path) async def list_documents(self) -> list[str]: return await self._api.list_documents() async def delete_document(self, source: str) -> None: await self._api.delete_document(source) class TTSService: def __init__(self, config: AppConfig): self._voice = config.tts_voice self._edge_voice = config.tts_edge_voice async def speak(self, text: str) -> str | None: """크로스플랫폼 TTS. macOS: say→edge-tts→pyttsx3 / Windows: edge-tts→pyttsx3""" if not text: return None if platform.system() == "Darwin": try: tmp = tempfile.NamedTemporaryFile(suffix=".aiff", delete=False) tmp.close() await asyncio.to_thread( subprocess.run, ["say", "-v", self._voice, "-o", tmp.name, text], check=True, capture_output=True, ) return tmp.name except Exception: pass try: import edge_tts tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) tmp.close() await edge_tts.Communicate(text, self._edge_voice).save(tmp.name) return tmp.name except Exception: pass try: import pyttsx3 tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) tmp.close() def _save(): engine = pyttsx3.init() engine.save_to_file(text, tmp.name) engine.runAndWait() await asyncio.to_thread(_save) return tmp.name except Exception: return None