"""Runtime wiring for capture, VAD, segmentation, and durable clips.""" from __future__ import annotations import logging import time from collections.abc import Callable, Iterable from datetime import datetime from pathlib import Path from typing import Protocol import numpy as np from mic_clipper.audio_input import PulseAudioInput from mic_clipper.segmenter import Audio, End, Segmenter, Start from mic_clipper.storage import ClipWriter, OpenClip from mic_clipper.vad import SileroVad LOGGER = logging.getLogger(__name__) class Vad(Protocol): def probability(self, samples: np.ndarray) -> float: ... class Writer(Protocol): def open(self, started_at: datetime) -> OpenClip: ... def record_frames( frames: Iterable[np.ndarray], *, vad: Vad, writer: Writer, segmenter: Segmenter, now: Callable[[], datetime], threshold: float = 0.5, ) -> None: """Record one finite or failing frame stream, closing an active encoder on exit.""" clip: OpenClip | None = None try: for samples in frames: events = segmenter.push(samples, vad.probability(samples) >= threshold, now()) for event in events: if isinstance(event, Start): clip = writer.open(event.started_at) try: clip.write(event.samples) except BaseException: clip.abort() clip = None raise elif isinstance(event, Audio): if clip is None: raise RuntimeError("segment audio arrived without an active encoder") clip.write(event.samples) elif isinstance(event, End): if clip is None: raise RuntimeError("segment end arrived without an active encoder") clip.close() clip = None finally: if clip is not None: clip.close() def run_forever(root: Path, *, threshold: float = 0.5) -> None: """Recreate all live components after a transient audio-service failure.""" retry_seconds = 1 while True: try: record_frames( PulseAudioInput().frames(), vad=SileroVad(), writer=ClipWriter(root), segmenter=Segmenter(pre_roll_seconds=0.5, silence_seconds=1.5), now=lambda: datetime.now().astimezone(), threshold=threshold, ) raise RuntimeError("Pulse input ended unexpectedly") except KeyboardInterrupt: LOGGER.info("microphone clip recorder stopped") return except Exception as error: LOGGER.warning( "audio recorder unavailable (%s); retrying in %s seconds", error, retry_seconds, ) time.sleep(retry_seconds) retry_seconds = min(retry_seconds * 2, 60)