""" Segment recording & overrides — the review foundation. During a translation job, every (source → translation) pair is recorded so it can be persisted per job (translation_segments table), reviewed and edited side-by-side, and re-applied when rebuilding the document. Overrides are the inverse path: human-reviewed translations (approved or edited segments) applied verbatim on rebuild — no provider call, no drift. """ from typing import Dict, List, Optional, Tuple from core.logging import get_logger logger = get_logger(__name__) class SegmentRecorder: """Collects unique (source, translation) pairs in document order.""" def __init__(self): self._pairs: List[Tuple[str, str]] = [] self._seen: set = set() def record_pair(self, source: str, translation: str) -> None: if not source or not source.strip(): return stripped = source.strip() # Identity pairs carry no review value ( untranslated or unchanged # text) — skip them so the review list stays actionable. if not translation or not translation.strip() or translation.strip() == stripped: return if stripped in self._seen: return self._seen.add(stripped) self._pairs.append((stripped, translation.strip())) def record_pairs(self, pairs) -> None: for source, translation in pairs: self.record_pair(source, translation) def get_pairs(self) -> List[Tuple[str, str]]: return list(self._pairs) def __len__(self) -> int: return len(self._pairs) def apply_overrides( texts: List[str], overrides: Optional[Dict[str, str]], ) -> Tuple[Dict[int, str], List[int]]: """Split a batch: ({index: override_translation}, [indices to translate]). Overrides are keyed by the stripped source text (that is how segments are persisted). Identity overrides are treated as "no override" so the normal pipeline (TM → provider) stays in charge. """ if not overrides: return {}, list(range(len(texts))) hits: Dict[int, str] = {} misses: List[int] = [] for i, text in enumerate(texts): if not text or not text.strip(): misses.append(i) continue override = overrides.get(text.strip()) if override is not None and override.strip() and override.strip() != text.strip(): hits[i] = override.strip() else: misses.append(i) return hits, misses