#!/usr/bin/env python3 """Extract Kṛṣṇa Book Volume 2 chapters from other.html. Volume 2 has 35 chapters (KB 1970-2-1 through KB 1970-2-35). Each chapter is a prose chapter (no verses) — just paragraphs of purport text. The chapters are delimited by p13 bookmark tags: "KB 1970-2-N / Title" Each chapter starts with a p27 heading: "N /Title" Body paragraphs are in p6 (primary) and p28 classes. Chapter end markers: "Thus ends the Bhaktivedanta purport of the Nth Chapter..." Output schema (one JSON per chapter): { "book": "kb", "volume": 2, "chapter": 1, "title": "The Story of the Syamantaka Jewel", "type": "chapter", "paragraphs": ["
...
", ...] } Output: lib/jsons/books/kb/vol-2/chapters/01.json through 35.json Usage: python3 scripts/kb_extract_vol2_chapters.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: """Extract inner HTML from a tag, decode ScaGoudy, preserve formatting tags.""" inner = "".join(str(child) for child in tag.children) inner = decode_scagoudy(inner) # Remove span tags with CSS classes (RTF conversion artifacts) 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 blocks # that sometimes appear within chapter prose — e.g., quoted verses) # --------------------------------------------------------------------------- VERSE_PADA_CLASSES = frozenset(["p4", "p5", "p10", "p19", "p24"]) # Primary body paragraph classes BODY_CLASSES = frozenset(["p6", "p28", "p23"]) # Chapter heading class (bold title like "1 /The Story of the Syamantaka Jewel") HEADING_CLASS = "p27" # Bookmark class BOOKMARK_CLASS = "p13" # Volume header class VOLUME_HEADER_CLASS = "p12" # Classes to skip entirely SKIP_CLASSES = frozenset( [ "p9", # empty / decorative breaks "p11", # empty breaks "p15", # empty breaks "p17", # empty breaks "p20", # empty breaks "p14", # section labels "p16", # "Not for sale in India" "p25", # handwritten notes marker "p26", # Apple Corps header "p29", # image descriptions (color plate references) "p30", # image descriptions ] ) def is_empty(tag: Tag) -> bool: text = tag.get_text().strip() return not text or text == "\xa0" # --------------------------------------------------------------------------- # Extraction # --------------------------------------------------------------------------- def extract_vol2_chapters(soup: BeautifulSoup): """Extract all 35 chapters of Volume 2. Returns a list of chapter dicts. """ all_paragraphs = list(soup.find_all("p")) # Step 1: Find all Vol 2 chapter bookmark positions # Format: "KB 1970-2-N / Title" chapter_bookmarks = [] # (paragraph_index, chapter_num, title) for i, p in enumerate(all_paragraphs): cls = get_css_class(p) if cls != BOOKMARK_CLASS: continue text = decode_text(p) m = re.match(r"KB\s+1970-2-(\d+)\s*/\s*(.+)", text) if m: ch_num = int(m.group(1)) title = m.group(2).strip() chapter_bookmarks.append((i, ch_num, title)) if not chapter_bookmarks: print("ERROR: No Vol 2 chapter bookmarks found!") return [] print(f" Found {len(chapter_bookmarks)} Vol 2 chapter bookmarks") # Step 2: Find the end boundary — the next book section or end of file # Look for the next p12 volume header that is NOT KB Vol 2 vol2_end = len(all_paragraphs) # Start searching after the last Vol 2 chapter bookmark last_bookmark_pos = chapter_bookmarks[-1][0] for i in range(last_bookmark_pos + 1, len(all_paragraphs)): p = all_paragraphs[i] cls = get_css_class(p) if cls == VOLUME_HEADER_CLASS: text = decode_text(p) # If it's a different book header (not KB), that's our end if "Volume 2" not in text: vol2_end = i break # Step 3: Extract each chapter chapters = [] for idx, (bookmark_pos, ch_num, title) in enumerate(chapter_bookmarks): # Chapter content starts after the bookmark content_start = bookmark_pos + 1 # Chapter content ends at the next chapter's bookmark (or end boundary) if idx + 1 < len(chapter_bookmarks): content_end = chapter_bookmarks[idx + 1][0] else: content_end = vol2_end # Extract paragraphs for this chapter paragraphs = extract_chapter_paragraphs( all_paragraphs, content_start, content_end ) chapters.append( { "book": "kb", "volume": 2, "chapter": ch_num, "title": title, "type": "chapter", "paragraphs": paragraphs, } ) return chapters def extract_chapter_paragraphs( all_paragraphs: list, start_idx: int, end_idx: int, ) -> list[str]: """Extract the prose paragraphs for a single chapter. Handles: - Regular body paragraphs (p6, p28) - Quoted Sanskrit verse blocks (p4, p5, p19, p24) → grouped into centered italic - Chapter heading (p27) → included as first paragraph - Chapter end marker ("Thus ends...") → included as last paragraph (italic) - Skips empty/decorative paragraphs """ result: list[str] = [] verse_accumulator: list[str] = [] def flush_verse(): if verse_accumulator: joined = "