#!/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()