Files
watch_sceen_ai/wsai/config.py
EJClaw 63fcfb7ba2 feat(stt): real Korean STT via persistent faster-whisper worker
Step 3 (귀): add WhisperSTT + whisper_worker, a warm out-of-venv worker
mirroring the MeloTTS shape (whisper312 venv, small/int8 on CPU). transcribe()
closes the voice round trip (MeloTTS wav -> whisper text); utterances() turns an
injected audio_source into Utterances (Discord voice feed pending). Wired into
factory as WSAI_STT=whisper.

Also address the arbiter's TTS follow-ups:
- melo worker error handling: capture stderr (drained in a bounded background
  task so the pipe can't fill), surface the real failure cause, and defend
  against an empty/invalid ready line instead of dying on JSONDecodeError.
- pipeline pre-warm: load slow backends (warmup()) at startup so the first
  utterance is answered warm; a warmup failure is logged, not fatal.

Verified: real TTS->STT round trip recovers the sentence near-perfectly;
warm transcribe ~1.2s (CPU). 12 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-18 18:29:32 +09:00

63 lines
2.2 KiB
Python

"""Configuration. Each field names a backend; the factory maps names -> classes.
Defaults are all "mock" so the skeleton runs out of the box. Flip individual
fields (via env or code) as real backends land.
Env overrides (optional):
WSAI_SOURCE, WSAI_VISION, WSAI_STT, WSAI_TTS, WSAI_BRAIN, WSAI_TEXT
WSAI_CAPTURE_INTERVAL
"""
from __future__ import annotations
import os
from dataclasses import dataclass
@dataclass
class Settings:
source: str | None = "mock" # mock | mss | None (eyes-free)
vision: str | None = "mock" # mock | claude | None (eyes-free)
stt: str | None = "mock" # mock | whisper | None
tts: str | None = "mock" # mock | melo | None
brain: str = "mock" # mock | claude
text: str | None = None # None | (discord)
capture_interval: float = 1.5
anthropic_model: str = "claude-sonnet-4-5"
@classmethod
def from_env(cls) -> "Settings":
def opt(name: str, default):
v = os.environ.get(name)
return default if v is None else (None if v.lower() == "none" else v)
return cls(
source=opt("WSAI_SOURCE", "mock"),
vision=opt("WSAI_VISION", "mock"),
stt=opt("WSAI_STT", "mock"),
tts=opt("WSAI_TTS", "mock"),
brain=opt("WSAI_BRAIN", "mock"),
text=opt("WSAI_TEXT", None),
capture_interval=float(os.environ.get("WSAI_CAPTURE_INTERVAL", "1.5")),
)
@classmethod
def mock(cls) -> "Settings":
return cls()
@classmethod
def live(cls) -> "Settings":
"""A realistic local config: capture this screen, Claude eyes+brain,
mock voice (until STT/TTS backends are wired)."""
return cls(source="mss", vision="claude", brain="claude", stt="mock", tts="mock")
@classmethod
def voice(cls) -> "Settings":
"""Eyes-free voice loop: no screen share, just STT -> Brain -> TTS.
Screen capture is deferred, so source/vision are off. Backends default
to mock so it runs out of the box; flip stt/tts/brain to real ones as
they land."""
return cls(source=None, vision=None, stt="mock", tts="mock", brain="mock")