Files
watch_sceen_ai/wsai/factory.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

97 lines
2.4 KiB
Python

"""Build a Pipeline from Settings. This is the single place that knows which
concrete class each config name maps to, so adding a backend = one line here."""
from __future__ import annotations
from .config import Settings
from .monitor import Monitor
from .pipeline import Pipeline
def build(settings: Settings, monitor: Monitor | None = None) -> Pipeline:
pipe = Pipeline(
source=_source(settings),
vision=_vision(settings),
brain=_brain(settings),
stt=_stt(settings),
tts=_tts(settings),
text_channel=_text(settings),
monitor=monitor,
)
if monitor is not None:
monitor.set_components(
{
"source": settings.source or "none",
"vision": settings.vision or "none",
"stt": settings.stt or "none",
"brain": settings.brain,
"tts": settings.tts or "none",
"text": settings.text or "none",
}
)
return pipe
def _source(s: Settings):
if s.source in (None, "none"):
return None
if s.source == "mss":
from .backends.capture_mss import MSSFrameSource
return MSSFrameSource(interval=s.capture_interval)
from .backends.mock import MockFrameSource
return MockFrameSource(interval=s.capture_interval)
def _vision(s: Settings):
if s.vision in (None, "none"):
return None
if s.vision == "claude":
from .backends.claude import ClaudeVision
return ClaudeVision(model=s.anthropic_model)
from .backends.mock import MockVision
return MockVision()
def _brain(s: Settings):
if s.brain == "claude":
from .backends.claude import ClaudeBrain
return ClaudeBrain(model=s.anthropic_model)
from .backends.mock import MockBrain
return MockBrain()
def _stt(s: Settings):
if s.stt in (None, "none"):
return None
if s.stt == "whisper":
from .backends.whisper import WhisperSTT
return WhisperSTT()
from .backends.mock import MockSTT
return MockSTT()
def _tts(s: Settings):
if s.tts in (None, "none"):
return None
if s.tts == "melo":
from .backends.melo import MeloTTS
return MeloTTS()
from .backends.mock import MockTTS
return MockTTS()
def _text(s: Settings):
if s.text in (None, "none"):
return None
raise NotImplementedError("discord text channel backend not implemented yet")