"""Pericope models for chunk-based translation.""" from __future__ import annotations from datetime import datetime from typing import TYPE_CHECKING, Any from pydantic import BaseModel, Field, computed_field if TYPE_CHECKING: from src.models.verse import VerseReference class Pericope(BaseModel): """A cohesive unit of biblical text (paragraph/section). Pericopes are the natural divisions of biblical text used for chunk-based translation to ensure smooth verse transitions. """ id: str = Field(..., description="Unique pericope identifier") title: str | None = Field(None, description="Section heading (English)") title_ja: str | None = Field(None, description="Section heading (Japanese)") start_chapter: int = Field(..., ge=1) start_verse: int = Field(..., ge=1) end_chapter: int = Field(..., ge=1) end_verse: int = Field(..., ge=1) verses: list[str] = Field( ..., description="List of verse refs like '1:1', '1:2'" ) theme: str | None = Field(None, description="Thematic summary") auto_generated: bool = Field( default=False, description="True if generated from paragraph data" ) @computed_field @property def verse_count(self) -> int: """Get the number of verses in this pericope.""" return len(self.verses) @computed_field @property def is_single_chapter(self) -> bool: """Check if pericope spans only one chapter.""" return self.start_chapter == self.end_chapter def contains(self, chapter: int, verse: int) -> bool: """Check if a verse is in this pericope. Args: chapter: Chapter number. verse: Verse number. Returns: True if the verse is within this pericope. """ ref = f"{chapter}:{verse}" return ref in self.verses def get_verse_refs(self, book: str) -> list[VerseReference]: """Get VerseReference objects for all verses. Args: book: Book name to use in references. Returns: List of VerseReference objects. """ from src.models.verse import VerseReference refs = [] for v in self.verses: chapter, verse = v.split(":") refs.append( VerseReference(book=book, chapter=int(chapter), verse=int(verse)) ) return refs def get_display_range(self) -> str: """Get a human-readable verse range string. Returns: String like '1:1-18' or '1:1-2:12'. """ if self.is_single_chapter: return f"{self.start_chapter}:{self.start_verse}-{self.end_verse}" return ( f"{self.start_chapter}:{self.start_verse}-" f"{self.end_chapter}:{self.end_verse}" ) class PericopeIndex(BaseModel): """Index of all pericopes for a book.""" book: str = Field(..., description="Book name") name: str = Field(..., description="Full book name") pericopes: list[Pericope] = Field(default_factory=list) sblgnt_paragraphs: list[dict] | None = Field( None, description="Raw SBLGNT paragraph data" ) def find_pericope(self, chapter: int, verse: int) -> Pericope | None: """Find the pericope containing a verse. Args: chapter: Chapter number. verse: Verse number. Returns: The Pericope containing the verse, or None if not found. """ for p in self.pericopes: if p.contains(chapter, verse): return p return None def get_pericope_by_id(self, pericope_id: str) -> Pericope | None: """Get a pericope by its ID. Args: pericope_id: The pericope identifier. Returns: The Pericope if found, None otherwise. """ for p in self.pericopes: if p.id == pericope_id: return p return None def get_chapter_pericopes(self, chapter: int) -> list[Pericope]: """Get all pericopes that include verses from a chapter. Args: chapter: Chapter number. Returns: List of pericopes that contain verses from the chapter. """ return [ p for p in self.pericopes if p.start_chapter <= chapter <= p.end_chapter ] class ChunkTranslationOutput(BaseModel): """Output from chunk-based translation.""" pericope_id: str = Field(..., description="Source pericope ID") book: str = Field(..., description="Book name") translations: dict[str, str] = Field( ..., description="Verse ref to translation mapping" ) model: str = Field(..., description="Model used for translation") timestamp: datetime = Field(default_factory=datetime.utcnow) coherence_note: str | None = Field( None, description="Note about translation coherence" ) @computed_field @property def verse_count(self) -> int: """Get number of verses translated.""" return len(self.translations) def get_verse_translation(self, chapter: int, verse: int) -> str | None: """Get translation for a specific verse. Args: chapter: Chapter number. verse: Verse number. Returns: Translation text or None if not found. """ ref = f"{chapter}:{verse}" return self.translations.get(ref) class ChunkTranslationContext(BaseModel): """Full context for translating a pericope/chunk. Contains all verses in the pericope plus surrounding context for cohesive translation. """ pericope: Pericope = Field(..., description="The pericope being translated") book: str = Field(..., description="Book name") verses: list[Any] = Field(..., description="All verses in the pericope") preceding_verses: list[Any] = Field( default_factory=list, description="Context before pericope" ) following_verses: list[Any] = Field( default_factory=list, description="Context after pericope" ) chapter_summary: Any = None book_metadata: Any = None glossary_terms: list[Any] = Field(default_factory=list) model_config = {"arbitrary_types_allowed": True} @property def verse_refs(self) -> list[str]: """Get list of verse reference strings like '1:1'.""" return self.pericope.verses @property def source_texts(self) -> dict[str, str]: """Get mapping of verse ref to Greek/Hebrew source text.""" texts = {} for verse in self.verses: ref = f"{verse.reference.chapter}:{verse.reference.verse}" source = verse.greek_text or verse.hebrew_text or "" if source: texts[ref] = source return texts def get_combined_source_text(self) -> str: """Get all source texts combined for the pericope.""" texts = [] for verse in self.verses: source = verse.greek_text or verse.hebrew_text if source: ref = f"{verse.reference.chapter}:{verse.reference.verse}" texts.append(f"[{ref}] {source}") return "\n".join(texts)