Adds a MeloTTS backend that runs the model in its own melo311 interpreter as a long-lived worker (melo_worker.py), loaded once and fed synthesis requests over a stdin/stdout JSON protocol. fd1 is split from fd2 in the worker so MeloTTS's stdout progress chatter can't corrupt the protocol. Each speak() writes a wav and hands the path to a pluggable sink (the Discord voice step will swap in "play into the call"). factory wires tts=melo; pipeline.aclose now also tears down the tts worker. Verified (CPU): model load ~7.9s once, then a short reply synthesizes in ~0.86s (within the ~1s budget); wav is valid 44.1kHz PCM. GPU (cuda) is selectable via WSAI_MELO_DEVICE for lower latency, pending GPU approval. 7 smoke tests still pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
93 lines
2.3 KiB
Python
93 lines
2.3 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
|
|
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")
|