"""Configuration loader for the radio show audio processor.""" from pathlib import Path from dataclasses import dataclass, field import yaml @dataclass class ShowConfig: name: str = "The Computer Guru Show" host: str = "Mike Swanson" typical_duration_minutes: int = 120 segment_count: int = 6 has_commercials: bool = True @dataclass class AudioConfig: whisper_model: str = "large-v3" whisper_language: str = "en" output_format: str = "mp3" output_bitrate: str = "192k" normalize: bool = True crossfade_ms: int = 500 @dataclass class DetectionWeights: fingerprint_match: float = 0.30 speaker_identity: float = 0.25 audio_characteristics: float = 0.20 break_pattern: float = 0.15 structural_heuristic: float = 0.10 @dataclass class SegmentDetectionConfig: fingerprint_db: str = "element-library/fingerprints.db" fingerprint_match_threshold: float = 0.85 discover_unknown_elements: bool = True min_element_duration_s: float = 1.0 max_element_duration_s: float = 30.0 cluster_similarity_threshold: float = 0.90 min_cluster_occurrences: int = 3 min_break_duration_s: int = 30 max_break_duration_s: int = 300 silence_threshold_db: int = -40 confidence_threshold: float = 0.70 weights: DetectionWeights = field(default_factory=DetectionWeights) @dataclass class DiarizationConfig: min_speakers: int = 1 max_speakers: int = 6 voice_profiles_dir: str = "voice-profiles/" host_match_threshold: float = 0.75 @dataclass class LLMConfig: model: str = "qwen3:14b" ollama_host: str = "http://localhost:11434" @dataclass class PathsConfig: episodes_dir: str = "episodes/" voice_profiles: str = "voice-profiles/" element_library: str = "element-library/" output_dir: str = "processed/" @dataclass class ArchiveConfig: server: str = "172.16.3.10" path: str = "/home/gurushow/public_html/archive/" elements_path: str = "/home/gurushow/public_html/archive/Radio/Elements/" @dataclass class Config: show: ShowConfig = field(default_factory=ShowConfig) audio: AudioConfig = field(default_factory=AudioConfig) segment_detection: SegmentDetectionConfig = field(default_factory=SegmentDetectionConfig) diarization: DiarizationConfig = field(default_factory=DiarizationConfig) llm: LLMConfig = field(default_factory=LLMConfig) paths: PathsConfig = field(default_factory=PathsConfig) archive: ArchiveConfig = field(default_factory=ArchiveConfig) base_dir: Path = field(default_factory=lambda: Path.cwd()) def resolve_path(self, relative: str) -> Path: return self.base_dir / relative def load_config(config_path: str | Path | None = None) -> Config: if config_path is None: config_path = Path(__file__).parent.parent / "config.yaml" config_path = Path(config_path) if not config_path.exists(): return Config(base_dir=config_path.parent) with open(config_path) as f: raw = yaml.safe_load(f) or {} config = Config(base_dir=config_path.parent) if "show" in raw: config.show = ShowConfig(**raw["show"]) if "audio" in raw: config.audio = AudioConfig(**raw["audio"]) if "segment_detection" in raw: sd = raw["segment_detection"] weights = DetectionWeights(**sd.pop("weights", {})) config.segment_detection = SegmentDetectionConfig(weights=weights, **sd) if "diarization" in raw: config.diarization = DiarizationConfig(**raw["diarization"]) if "llm" in raw: config.llm = LLMConfig(**raw["llm"]) if "paths" in raw: config.paths = PathsConfig(**raw["paths"]) if "archive" in raw: config.archive = ArchiveConfig(**raw["archive"]) return config