Files
youlbot-webui/services.py
T
shinalok d81a2f5888 Phase 26: P1 architecture refactor — DI container, service layer, async callbacks
- config.py: APIConfig + AppConfig dataclasses, env vars centralized
- api_client.py: APIClientProtocol (Protocol) + HTTPAPIClient class, remove module-level globals
- services.py: ChatService, DocumentService, TTSService (TTS moved from app.py)
- container.py: manual DI container with lazy singleton properties
- app.py: all callbacks converted to async, asyncio.run() fully removed, container wired in
- .env.example: add TTS_EDGE_VOICE entry
- ROADMAP.md: P0/P1 checklist updated to reflect completed work

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 17:36:35 +09:00

94 lines
2.8 KiB
Python

"""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