"""Explicit local-file input adapter with safe error reporting.""" from __future__ import annotations from collections.abc import Iterator from pathlib import Path from yovision_brain.config import BrainInputConfig from .models import CancellationToken, InputError, InputPacket class LocalFileInput: def __init__(self, config: BrainInputConfig) -> None: if config.source.kind != "local_file" or config.source.path is None: raise ValueError("LocalFileInput requires a local_file source config") self._config = config self._path = config.source.path @property def source_label(self) -> str: """Return a safe label rather than exposing the internal absolute path.""" return self._path.name def packets(self, cancellation: CancellationToken | None = None) -> Iterator[InputPacket]: profile = self._config.profile source = self._config.source try: stream = self._path.open("rb") except FileNotFoundError as exc: raise InputError(f"local video source not found: {self.source_label}") from exc except OSError as exc: raise InputError(f"local video source cannot be opened: {self.source_label}: {exc.strerror}") from exc with stream: sequence = 0 while cancellation is None or not cancellation.cancelled: try: payload = stream.read(source.chunk_size) except OSError as exc: raise InputError(f"local video source read failed: {self.source_label}: {exc.strerror}") from exc if not payload: return yield InputPacket( sequence=sequence, timestamp_ns=None, logical_device_id=self._config.logical_device_id, profile_id=profile.profile_id, width=profile.width, height=profile.height, media_format="container-bytes", payload=payload, ) sequence += 1