from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional, Dict, Literal
import yaml
from .api.message import ConfigError
@dataclass(frozen=True)
class Config:
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]
"""Global log level, as passed from commandline."""
source: Literal["alsa", "pulse", "pyaudio", "wave"]
""":class:`.api.model.AudioSource`"""
sink: Literal["alsa", "pulse", "pyaudio", "wave"]
""":class:`.api.model.AudioSink`"""
recognizer: Literal["sphinx", "vosk", "whisper"]
""":class:`.api.model.RecognizerModel`"""
processor: Literal["noop", "ollama", "gpt4all"]
""":class:`.api.model.ProcessorModel`"""
synthesizer: Literal["espeak", "coqui"]
""":class:`.api.model.SynthesizerModel`"""
alsa_source: Dict = field(default_factory=dict)
""":class:`.audio.alsa.AlsaRecorder`"""
alsa_sink: Dict = field(default_factory=dict)
""":class:`.audio.alsa.AlsaPlayer`"""
pulse_source: Dict = field(default_factory=dict)
""":class:`.audio.pulse.PulseRecorder`"""
pulse_sink: Dict = field(default_factory=dict)
""":class:`.audio.pulse.PulsePlayer`"""
pyaudio_source: Dict = field(default_factory=dict)
""":class:`.audio.pyaudio.PyAudioRecorder`"""
pyaudio_sink: Dict = field(default_factory=dict)
""":class:`.audio.pyaudio.PyAudioPlayer`"""
wave_source: Dict = field(default_factory=dict)
""":class:`.audio.wave.WavePlayer`"""
wave_sink: Dict = field(default_factory=dict)
""":class:`.audio.wave.WaveRecorder`"""
speech_segmenter: Literal["simple", "median", "band", "sphinx"] = "simple"
""":class:`.api.model.SpeechSegmenter`"""
speech_buffer_limit: float = 30.0
"""Limit on a possibly spurious single utterance in seconds, enforced by :class:`.api.model.SpeechSegmenter`."""
speech_pause_limit: float = 30.0
"""Limit on silence during an utterance in seconds, enforced by :class:`.api.model.SpeechSegmenter`."""
simple_speech_segmenter: Dict = field(default_factory=dict)
""":class:`.recognizer.speech.median.SimpleSegmenter`"""
median_speech_segmenter: Dict = field(default_factory=dict)
""":class:`.recognizer.speech.median.MedianSegmenter`"""
band_speech_segmenter: Dict = field(default_factory=dict)
""":class:`.recognizer.speech.rosa.BandSegmenter`"""
sphinx_speech_segmenter: Dict = field(default_factory=dict)
""":class:`.recognizer.speech.sphinx.SphinxSegmenter`"""
sphinx_recognizer: Dict = field(default_factory=dict)
""":class:`.recognizer.sphinx.SphinxRecognizer`"""
vosk_recognizer: Dict = field(default_factory=dict)
""":class:`.recognizer.vosk.VoskRecognizer`"""
whisper_recognizer: Dict = field(default_factory=dict)
""":class:`.recognizer.whisper.WhisperRecognizer`"""
ollama_processor: Dict = field(default_factory=dict)
""":class:`.processor.ollama.OllamaProcessorModel`"""
gpt4all_processor: Dict = field(default_factory=dict)
""":class:`.processor.gpt4all.Gpt4AllProcessorModel`"""
sentence_segmenter: Literal["split", "sbd"] = "split"
""":class:`.api.model.SentenceSegmenter`"""
split_sentence_segmenter: Dict = field(default_factory=dict)
""":class:`.synthesizer.sentence.segmenter.SentenceSplitSegmenter`"""
sbd_sentence_segmenter: Dict = field(default_factory=dict)
""":class:`.synthesizer.sentence.sbd.SentenceBoundarySegmenter`"""
feedback: Literal["noop", "speech", "beep"] = "beep"
""":class:`.api.model.OutputFilter`"""
beep_feedback: Dict = field(default_factory=dict)
""":class:`.synthesizer.feedback.beep.BeepFeedbackGenerator`"""
espeak_synthesizer: Dict = field(default_factory=dict)
""":class:`.synthesizer.espeak.EspeakSynthesizer`"""
coqui_synthesizer: Dict = field(default_factory=dict)
""":class:`.synthesizer.coqui.CoquiSynthesizerModel`"""
keywords: Dict[str, str] = field(default_factory=dict)
"""Set alternate hotwords for ``start``, ``reset``, ``commit``, or ``stop``."""
logging: Optional[Dict] = None
"""`Logging config <https://docs.python.org/3/library/logging.config.html>`__, applied by each process."""
@classmethod
def from_dict(cls, **kwargs: Any) -> "Config":
try:
return cls(**kwargs)
except TypeError as e:
raise ConfigError(f"Cannot apply config: {str(e)}") from None
@classmethod
def from_file(cls, filename: Optional[Path], **kwargs: Any) -> "Config":
if filename is None:
return cls.from_dict(**kwargs)
try:
with filename.open("rt", encoding="utf-8", errors="strict") as fp:
doc: Any = yaml.safe_load(fp)
if isinstance(doc, dict):
return cls.from_dict(**doc, **kwargs)
else:
raise ValueError(f"Expected YAML object, got {type(doc)}")
except (OSError, yaml.YAMLError, ValueError) as e:
raise ConfigError(f"Cannot load config from '{filename}': {str(e)}") from None