310 lines
9.6 KiB
Python
310 lines
9.6 KiB
Python
#!/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'<span[^>]*class="[^"]*"[^>]*>(.*?)</span>', 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 = "<br>".join(verse_accumulator)
|
||
intro_paragraphs.append(
|
||
f'<div style="text-align:center"><i>{joined}</i></div>'
|
||
)
|
||
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 <br> 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()
|