#!/usr/bin/env python3 """Extract Kṛṣṇa Book Volume 1 introductory segments from other.html. The Volume 1 front matter (lines ~772–901) contains 5 segments before Chapter 1: 1. Flap intro — book jacket text 2. Dedication — "To My Father, Gour Mohon De (1849-1930)" 3. Words from Apple — George Harrison's introduction 4. Preface — Prabhupāda's preface 5. Introduction — Prabhupāda's introduction Each segment is extracted into a JSON file with a common schema: { "slug": "flap-intro", "title": "Flap Introduction", "type": "intro-segment", "book": "kb", "volume": 1, "order": 1, "paragraphs": ["
...
", ...] } Output: lib/jsons/books/kb/vol-1/intro/*.json Usage: python3 scripts/kb_extract_vol1_intro.py """ import json import os import re import sys from bs4 import BeautifulSoup, Tag # --------------------------------------------------------------------------- # ScaGoudy → IAST Unicode mapping # --------------------------------------------------------------------------- SCAGOUDY_TO_IAST = { "ä": "ā", "Ä": "Ā", "å": "ṛ", "Å": "Ṛ", "ñ": "ṣ", "Ñ": "Ṣ", "ö": "ṭ", "Ö": "Ṭ", "ë": "ṇ", "Ë": "Ṇ", "ò": "ḍ", "Ò": "Ḍ", "ç": "ś", "Ç": "Ś", "ì": "ṅ", "ï": "ñ", "ü": "ū", "Ü": "Ū", "é": "ī", "É": "Ī", "à": "ṁ", "ù": "ḥ", "è": "ṝ", "û": "ṁl", "ÿ": "ḷ", } def decode_scagoudy(text: str) -> str: for src, dst in SCAGOUDY_TO_IAST.items(): text = text.replace(src, dst) return text def decode_html_content(tag: Tag) -> str: inner = "".join(str(child) for child in tag.children) inner = decode_scagoudy(inner) inner = re.sub(r']*class="[^"]*"[^>]*>(.*?)', r"\1", inner) inner = re.sub(r"\s+", " ", inner).strip() return inner def decode_text(tag: Tag) -> str: return decode_scagoudy(tag.get_text().strip()) def get_css_class(tag: Tag) -> str: classes = tag.get("class", []) return classes[0] if classes else "" # CSS classes for Sanskrit verse pādas (center-aligned italic) VERSE_PADA_CLASSES = frozenset(["p4", "p5", "p10", "p19", "p24"]) def is_empty(tag: Tag) -> bool: """Check if a tag is empty / whitespace only.""" text = tag.get_text().strip() return not text or text == "\xa0" # --------------------------------------------------------------------------- # Segment definitions # --------------------------------------------------------------------------- SEGMENTS = [ { "slug": "flap-intro", "title": "Flap Introduction", "order": 1, "bookmark": "Kb 1970 /Flap intro", }, { "slug": "dedication", "title": "Dedication — To My Father, Gour Mohon De (1849–1930)", "order": 2, "bookmark": "Kb 1970 / To My Father, Gour Mohon De (1849-1930)", }, { "slug": "words-from-apple", "title": "Words from Apple — by George Harrison", "order": 3, "bookmark": "Kb 1970 / Words from Apple", }, { "slug": "preface", "title": "Preface", "order": 4, "bookmark": "Kb 1970 / Preface", }, { "slug": "introduction", "title": "Introduction", "order": 5, "bookmark": "Kb 1970 / Introduction", }, ] # --------------------------------------------------------------------------- # Extraction # --------------------------------------------------------------------------- def find_segment_ranges(soup: BeautifulSoup): """Find the start/end paragraph indices for each segment. Segments are delimited by p13 bookmark tags. The first chapter marker (KB 1970-1-1) ends the last intro segment. """ all_paragraphs = list(soup.find_all("p")) # Find all p13 bookmark indices within the KB volume 1 area bookmark_positions = [] # (index, text) for i, p in enumerate(all_paragraphs): cls = get_css_class(p) if cls == "p13": text = decode_text(p) bookmark_positions.append((i, text)) # Map each segment to its start/end paragraph indices ranges = {} for seg in SEGMENTS: # Find the bookmark that starts this segment start_idx = None for i, (pos, text) in enumerate(bookmark_positions): if seg["bookmark"] in text: # Content starts AFTER the bookmark tag start_idx = pos + 1 # End is the next bookmark if i + 1 < len(bookmark_positions): end_idx = bookmark_positions[i + 1][0] else: end_idx = len(all_paragraphs) ranges[seg["slug"]] = (start_idx, end_idx, all_paragraphs) break if start_idx is None: print( f" WARNING: Could not find bookmark for '{seg['slug']}': '{seg['bookmark']}'" ) return ranges def extract_segment_paragraphs( all_paragraphs: list, start_idx: int, end_idx: int, ) -> list[str]: """Extract paragraphs for a single segment, handling verse grouping.""" result: list[str] = [] verse_accumulator: list[str] = [] def flush_verse(): if verse_accumulator: joined = "