公開前の確認用サイト
日常訳NICHIJOYAKUEnglish ↗

OPEN SOURCE

software/src/context/pericope_generator.py

このサイトで読み、必要なファイルを保存できます。

日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。

← オープンソースの案内へ

software/src/context/pericope_generator.py368 行このファイルを保存 ↓

ZIPに含まれるものと同じ公開ソースです。行番号から、その行へのリンクを保存できます。

ファイルの確認情報SHA-256 c792fdb88e3edd816f4e4b22c5ca8cd967d4fd6664cff62c46f1d6bf2a73c33e
1"""Automatic pericope generation from SBLGNT paragraph markers.23Generates pericope boundaries from SBLGNT paragraph markers (¶) when4manually curated pericope data is not available.5"""67from __future__ import annotations89import json10import re11from pathlib import Path12from typing import TYPE_CHECKING1314from src.models.pericope import Pericope, PericopeIndex1516if TYPE_CHECKING:17    from src.data.loader import DataLoader181920# Paragraph marker in SBLGNT text21PARAGRAPH_MARKER = "¶"2223# Minimum verses in a pericope (avoid single-verse pericopes)24MIN_PERICOPE_SIZE = 22526# Maximum verses in a pericope (split if too large)27MAX_PERICOPE_SIZE = 25282930class PericopeGenerator:31    """Generates pericope boundaries from SBLGNT paragraph markers.3233    The SBLGNT (SBL Greek New Testament) includes paragraph markers (¶)34    that indicate natural text divisions. This generator uses those35    markers to create pericope boundaries for books that don't have36    manually curated pericope data.37    """3839    def __init__(40        self,41        data_dir: Path | str,42        loader: DataLoader | None = None,43    ) -> None:44        """Initialize the pericope generator.4546        Args:47            data_dir: Path to the data directory.48            loader: Optional DataLoader for accessing verse data.49        """50        self._data_dir = Path(data_dir)51        self._loader = loader5253    def _load_greek_data(self, book: str) -> dict:54        """Load Greek source data for a book.5556        Args:57            book: Book name.5859        Returns:60            Dictionary of Greek verse data.61        """62        path = self._data_dir / "sources" / "greek" / f"{book}.json"63        if not path.exists():64            return {}6566        with path.open("r", encoding="utf-8") as f:67            return json.load(f)6869    def _find_paragraph_markers(self, greek_data: dict) -> list[str]:70        """Find verse references that start with paragraph markers.7172        Args:73            greek_data: Dictionary of Greek verse data.7475        Returns:76            List of verse references (e.g., '1:1', '1:5') that begin paragraphs.77        """78        paragraph_starts = []7980        for ref, data in sorted(81            greek_data.items(),82            key=lambda x: self._ref_sort_key(x[0]),83        ):84            text = data.get("text", "") if isinstance(data, dict) else data85            if text and text.strip().startswith(PARAGRAPH_MARKER):86                paragraph_starts.append(ref)8788        return paragraph_starts8990    def _ref_sort_key(self, ref: str) -> tuple[int, int]:91        """Get sort key for a verse reference.9293        Args:94            ref: Verse reference like '1:1'.9596        Returns:97            Tuple of (chapter, verse) for sorting.98        """99        try:100            chapter, verse = ref.split(":")101            return (int(chapter), int(verse))102        except (ValueError, AttributeError):103            return (0, 0)104105    def _get_all_verses(self, greek_data: dict) -> list[str]:106        """Get all verse references in order.107108        Args:109            greek_data: Dictionary of Greek verse data.110111        Returns:112            Sorted list of verse references.113        """114        refs = list(greek_data.keys())115        return sorted(refs, key=self._ref_sort_key)116117    def _split_large_pericope(118        self,119        verses: list[str],120        max_size: int = MAX_PERICOPE_SIZE,121    ) -> list[list[str]]:122        """Split a large pericope into smaller chunks.123124        Args:125            verses: List of verse references.126            max_size: Maximum pericope size.127128        Returns:129            List of verse lists, each representing a pericope.130        """131        if len(verses) <= max_size:132            return [verses]133134        # Split into roughly equal parts135        num_parts = (len(verses) + max_size - 1) // max_size136        part_size = len(verses) // num_parts137138        parts = []139        for i in range(num_parts):140            start = i * part_size141            end = start + part_size if i < num_parts - 1 else len(verses)142            parts.append(verses[start:end])143144        return parts145146    def generate_pericopes(147        self,148        book: str,149        min_size: int = MIN_PERICOPE_SIZE,150        max_size: int = MAX_PERICOPE_SIZE,151    ) -> PericopeIndex:152        """Generate pericopes for a book from paragraph markers.153154        Args:155            book: Book name.156            min_size: Minimum pericope size.157            max_size: Maximum pericope size.158159        Returns:160            PericopeIndex containing generated pericopes.161        """162        greek_data = self._load_greek_data(book)163164        if not greek_data:165            return PericopeIndex(166                book=book,167                name=book.title(),168                pericopes=[],169            )170171        # Get all verses and paragraph markers172        all_verses = self._get_all_verses(greek_data)173        paragraph_starts = self._find_paragraph_markers(greek_data)174175        # Always treat verse 1:1 as a paragraph start if not already176        if all_verses and all_verses[0] not in paragraph_starts:177            paragraph_starts.insert(0, all_verses[0])178179        # Build pericopes between paragraph markers180        pericopes = []181        pericope_count = 0182183        for i, start_ref in enumerate(paragraph_starts):184            # Find end of this pericope (start of next, or end of book)185            if i + 1 < len(paragraph_starts):186                end_ref = paragraph_starts[i + 1]187                # Get verses up to but not including the next paragraph188                start_idx = all_verses.index(start_ref)189                end_idx = all_verses.index(end_ref)190                verses = all_verses[start_idx:end_idx]191            else:192                # Last pericope goes to end of book193                start_idx = all_verses.index(start_ref)194                verses = all_verses[start_idx:]195196            # Skip empty or too-small pericopes (merge with previous)197            if len(verses) < min_size:198                if pericopes:199                    # Merge with previous pericope200                    pericopes[-1].verses.extend(verses)201                    self._update_pericope_range(pericopes[-1])202                continue203204            # Split large pericopes205            verse_groups = self._split_large_pericope(verses, max_size)206207            for verse_group in verse_groups:208                pericope_count += 1209                pericope = self._create_pericope(210                    book=book,211                    verses=verse_group,212                    index=pericope_count,213                )214                pericopes.append(pericope)215216        return PericopeIndex(217            book=book,218            name=book.title(),219            pericopes=pericopes,220        )221222    def _create_pericope(223        self,224        book: str,225        verses: list[str],226        index: int,227    ) -> Pericope:228        """Create a Pericope object from a list of verses.229230        Args:231            book: Book name.232            verses: List of verse references.233            index: Pericope index number.234235        Returns:236            Pericope object.237        """238        start_chapter, start_verse = map(int, verses[0].split(":"))239        end_chapter, end_verse = map(int, verses[-1].split(":"))240241        return Pericope(242            id=f"{book}-auto-{index:03d}",243            title=None,244            title_ja=None,245            start_chapter=start_chapter,246            start_verse=start_verse,247            end_chapter=end_chapter,248            end_verse=end_verse,249            verses=verses,250            theme=None,251            auto_generated=True,252        )253254    def _update_pericope_range(self, pericope: Pericope) -> None:255        """Update pericope start/end based on verses list.256257        Args:258            pericope: Pericope to update.259        """260        if not pericope.verses:261            return262263        start_chapter, start_verse = map(int, pericope.verses[0].split(":"))264        end_chapter, end_verse = map(int, pericope.verses[-1].split(":"))265266        pericope.start_chapter = start_chapter267        pericope.start_verse = start_verse268        pericope.end_chapter = end_chapter269        pericope.end_verse = end_verse270271    def generate_and_save(272        self,273        book: str,274        output_path: Path | None = None,275    ) -> PericopeIndex:276        """Generate pericopes and save to file.277278        Args:279            book: Book name.280            output_path: Optional output path. Defaults to metadata directory.281282        Returns:283            Generated PericopeIndex.284        """285        index = self.generate_pericopes(book)286287        if output_path is None:288            output_path = self._data_dir / "metadata" / "pericopes_generated.json"289290        # Load existing generated pericopes291        if output_path.exists():292            with output_path.open("r", encoding="utf-8") as f:293                data = json.load(f)294        else:295            data = {}296297        # Update with new pericopes298        data[book] = {299            "name": index.name,300            "pericopes": [p.model_dump() for p in index.pericopes],301            "auto_generated": True,302        }303304        # Save305        output_path.parent.mkdir(parents=True, exist_ok=True)306        with output_path.open("w", encoding="utf-8") as f:307            json.dump(data, f, indent=2, ensure_ascii=False)308309        return index310311    def generate_all_books(self, books: list[str] | None = None) -> dict[str, PericopeIndex]:312        """Generate pericopes for all books.313314        Args:315            books: List of book names. If None, scans Greek data directory.316317        Returns:318            Dictionary mapping book names to PericopeIndex.319        """320        if books is None:321            greek_dir = self._data_dir / "sources" / "greek"322            if greek_dir.exists():323                books = [324                    f.stem for f in greek_dir.glob("*.json")325                ]326            else:327                books = []328329        result = {}330        for book in books:331            index = self.generate_pericopes(book)332            if index.pericopes:333                result[book] = index334335        return result336337338def merge_pericope_indexes(339    manual: PericopeIndex | None,340    generated: PericopeIndex | None,341) -> PericopeIndex | None:342    """Merge manual and generated pericope indexes.343344    Manual pericopes take precedence; generated fill gaps.345346    Args:347        manual: Manually curated pericope index.348        generated: Auto-generated pericope index.349350    Returns:351        Merged PericopeIndex, or None if both inputs are None.352    """353    if manual is None and generated is None:354        return None355356    if manual is None:357        return generated358359    if generated is None:360        return manual361362    # Manual takes full precedence if it has pericopes363    if manual.pericopes:364        return manual365366    # Use generated if manual is empty367    return generated368