import json from pathlib import Path from threading import RLock from typing import Dict, Optional from .models import SentenceBoundaryDocument def normalize_video_hash(video_hash: str) -> str: normalized = video_hash.strip().lower() if len(normalized) != 64 or any(char not in "0123456789abcdef" for char in normalized): raise ValueError("video_hash must be a SHA-256 hex digest") return normalized class BoundaryStore: """Read-only in-memory index loaded from a JSON file.""" def __init__(self, index_path: Path): self.index_path = Path(index_path) self._lock = RLock() self._documents: Dict[str, SentenceBoundaryDocument] = {} self.reload() def reload(self) -> None: raw = ( json.loads(self.index_path.read_text(encoding="utf-8")) if self.index_path.is_file() else {"videos": {}} ) entries = raw.get("videos", raw) if not isinstance(entries, dict): raise ValueError("index JSON must contain a 'videos' object") documents: Dict[str, SentenceBoundaryDocument] = {} for video_hash, entry in entries.items(): normalized_hash = normalize_video_hash(video_hash) if not isinstance(entry, dict): raise ValueError(f"entry for {normalized_hash} must be an object") document = SentenceBoundaryDocument( video_hash=normalized_hash, duration_ms=entry.get("duration_ms"), algorithm_version=entry.get("algorithm_version", "unknown"), sentences=entry.get("sentences", []), ) documents[normalized_hash] = document with self._lock: self._documents = documents def get(self, video_hash: str) -> Optional[SentenceBoundaryDocument]: normalized_hash = normalize_video_hash(video_hash) with self._lock: return self._documents.get(normalized_hash) def count(self) -> int: with self._lock: return len(self._documents)