#!/usr/bin/env python3 """Extract Kṛṣṇa Book introduction and build intro.json with chapter listing. Reads public/html/other.html to extract: 1. The introduction/preface text (lines ~713–771) 2. The full chapter listing from p13 bookmark tags (KB 1970-V-C / Title) 3. Chapter end markers ("Thus ends the Bhaktivedanta purport of the Nth Chapter...") Output: lib/jsons/books/kb/intro.json Usage: python3 scripts/kb_extract_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 "" # --------------------------------------------------------------------------- # Extract introduction paragraphs (lines ~713–771) # --------------------------------------------------------------------------- # CSS classes used for Sanskrit verse pādas in the introduction VERSE_PADA_CLASSES = frozenset(["p4", "p5", "p10"]) def extract_introduction(soup: BeautifulSoup) -> list[str]: """Extract the Kṛṣṇa Book introduction/preface paragraphs. Starts at the 'KṚṢṆA BOOK-Introduction' header and ends at the 'February 26th, 1970' date line (first occurrence — the undated typed document version). Sanskrit verse lines (p4/p5 classes) are grouped into a single center-aligned italic block. """ paragraphs = soup.find_all("p") intro_paragraphs: list[str] = [] in_intro = False found_date = False verse_accumulator: list[str] = [] def flush_verse(): """Join accumulated Sanskrit verse pādas into one formatted block.""" if verse_accumulator: joined = "
".join(verse_accumulator) intro_paragraphs.append( f'
{joined}
' ) verse_accumulator.clear() for p in paragraphs: cls = get_css_class(p) text = p.get_text().strip() # Start marker if not in_intro: if "BOOK-Introduction" in text or "BOOK\u2011Introduction" in text: in_intro = True continue # Also match the ScaGoudy-encoded version if "BOOK" in text and "Introduction" in text and "K" in text: decoded = decode_text(p) if "KṚṢṆA" in decoded and "Introduction" in decoded: in_intro = True continue continue # Skip the "Preface" subheader if text == "Preface" or text.startswith("*****"): continue # End marker — "February 26th, 1970" (first occurrence) if "February 26th, 1970" in text: flush_verse() found_date = True break # Skip empty paragraphs if not text or text == "\xa0": continue # Sanskrit verse pādas (p4, p5, p10) — accumulate into a single block # Use decode_html_content to preserve
tags within multi-line verse elements if cls in VERSE_PADA_CLASSES: html = decode_html_content(p) if html: verse_accumulator.append(html) continue # If we were accumulating verse lines and hit a non-verse paragraph, # flush the verse block first flush_verse() # Collect the paragraph html = decode_html_content(p) if html: intro_paragraphs.append(html) if not found_date: print(" WARNING: Did not find the end marker 'February 26th, 1970'") return intro_paragraphs # --------------------------------------------------------------------------- # Extract chapter listing from p13 bookmarks # --------------------------------------------------------------------------- def extract_chapters(soup: BeautifulSoup) -> list[dict]: """Extract the full chapter listing from p13 bookmark tags. Format: 'KB 1970-V-C / Title' where V=volume, C=chapter within volume. We produce a flat chapter numbering (1–89) across both volumes. Also finds chapter end markers to count paragraphs per chapter (useful for later extraction). """ chapters: list[dict] = [] chapter_num = 0 # Also extract chapter end markers for titles end_titles: dict[int, str] = {} for p in soup.find_all("p"): cls = get_css_class(p) text = decode_text(p) # "Thus ends the Bhaktivedanta purport of the First Chapter of Kṛṣṇa, "Title"" if "Thus ends" in text and "Chapter of" in text and "Kṛṣṇa" in text: # Extract chapter ordinal and title m = re.search( r"(?:the\s+)?(\w+)\s+Chapter\s+of\s+Kṛṣṇa,\s*[\"\"\"]*(.+?)[\"\"\"]*\.?\s*$", text, ) if m: # We don't need the ordinal — just the title for cross-referencing title_from_end = m.group(2).strip().rstrip(".") # Store by the chapter count we've seen so far (rough approximation) # Extract from p13 bookmarks for p in soup.find_all("p"): cls = get_css_class(p) if cls != "p13": continue text = decode_text(p) # Match: KB 1970-V-C / Title m = re.match(r"KB\s+1970-(\d+)-(\d+)\s*/\s*(.+)", text) if not m: continue volume = int(m.group(1)) chapter_in_vol = int(m.group(2)) title = m.group(3).strip() chapter_num += 1 chapters.append( { "number": chapter_num, "volume": volume, "chapterInVolume": chapter_in_vol, "title": title, } ) return chapters # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) html_path = os.path.join(base_dir, "public", "html", "other.html") output_path = os.path.join(base_dir, "lib", "jsons", "books", "kb", "intro.json") if not os.path.exists(html_path): print(f"ERROR: {html_path} not found!") sys.exit(1) print(f"Reading {html_path}...") with open(html_path, "r", encoding="utf-8") as f: soup = BeautifulSoup(f, "html.parser") # Extract introduction print("Extracting introduction...") intro_paragraphs = extract_introduction(soup) print(f" Found {len(intro_paragraphs)} introduction paragraphs") # Extract chapter listing print("Extracting chapter listing...") chapters = extract_chapters(soup) print(f" Found {len(chapters)} chapters") # Count per volume vol1 = [c for c in chapters if c["volume"] == 1] vol2 = [c for c in chapters if c["volume"] == 2] print(f" Volume 1: {len(vol1)} chapters") print(f" Volume 2: {len(vol2)} chapters") # Build intro.json # Strip volume/chapterInVolume from the chapter entries for the public JSON chapters_public = [{"number": c["number"], "title": c["title"]} for c in chapters] intro = { "book": "kb", "title": "Kṛṣṇa, The Supreme Personality of Godhead", "author": "His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda", "description": "A summary study of the Tenth Canto of Śrīmad-Bhāgavatam. Originally published in two volumes in 1970.", "totalChapters": len(chapters), "volumes": [ {"number": 1, "totalChapters": len(vol1)}, {"number": 2, "totalChapters": len(vol2)}, ], "chapters": chapters_public, "introduction": intro_paragraphs, } # Write os.makedirs(os.path.dirname(output_path), exist_ok=True) with open(output_path, "w", encoding="utf-8") as f: json.dump(intro, f, ensure_ascii=False, indent=2) print(f"\nWritten: {output_path}") # Summary print(f"\n--- Summary ---") print(f"Title: {intro['title']}") print(f"Chapters: {intro['totalChapters']}") print(f"Introduction paragraphs: {len(intro_paragraphs)}") # Show first few intro paragraphs print(f"\n--- Introduction (first 3 paragraphs) ---") for i, para in enumerate(intro_paragraphs[:3]): print(f" [{i + 1}] {para[:120]}...") # Show first and last chapters print(f"\n--- Chapters ---") for c in chapters[:5]: print(f" Ch {c['number']:>2} (Vol {c['volume']}): {c['title']}") print(f" ...") for c in chapters[-3:]: print(f" Ch {c['number']:>2} (Vol {c['volume']}): {c['title']}") if __name__ == "__main__": main()