feat: kb both volumes

This commit is contained in:
gosangam
2026-04-16 00:48:48 +05:30
parent abb7cc49d0
commit d88ababd76
15 changed files with 2298 additions and 40 deletions
+313
View File
@@ -0,0 +1,313 @@
#!/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": ["<p>...</p>", ...]
}
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'<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 ""
# 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 = "<br>".join(verse_accumulator)
result.append(f'<div style="text-align:center"><i>{joined}</i></div>')
verse_accumulator.clear()
for i in range(start_idx, end_idx):
p = all_paragraphs[i]
cls = get_css_class(p)
text = p.get_text().strip()
# Skip the segment's own heading (p12, p14, p22, p27 bold headers)
# These are the display titles like "Flap:", "Words From Apple", "Preface", etc.
# We keep them as the first paragraph if they have meaningful content
if cls in ("p12", "p14"):
# Volume header or section label — skip
continue
# Skip empty paragraphs
if is_empty(p):
continue
# Skip bookmark tags (p13) that might be nested
if cls == "p13":
continue
# Sanskrit verse pādas — accumulate
# 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
# Non-verse paragraph — flush any accumulated verse first
flush_verse()
# Extract the HTML content
html = decode_html_content(p)
if html:
result.append(html)
# Flush any trailing verse
flush_verse()
return result
# ---------------------------------------------------------------------------
# Output
# ---------------------------------------------------------------------------
def write_json(data, filepath):
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f" Written: {filepath}")
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_dir = os.path.join(base_dir, "lib", "jsons", "books", "kb", "vol-1", "intro")
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")
print("Finding segment boundaries...")
ranges = find_segment_ranges(soup)
print(f"Extracting {len(SEGMENTS)} introductory segments...\n")
for seg in SEGMENTS:
slug = seg["slug"]
if slug not in ranges:
print(f" ⏭ Skipping '{slug}' — bookmark not found")
continue
start_idx, end_idx, all_paragraphs = ranges[slug]
paragraphs = extract_segment_paragraphs(all_paragraphs, start_idx, end_idx)
doc = {
"slug": slug,
"title": seg["title"],
"type": "intro-segment",
"book": "kb",
"volume": 1,
"order": seg["order"],
"paragraphs": paragraphs,
}
filepath = os.path.join(output_dir, f"{slug}.json")
write_json(doc, filepath)
print(f" {seg['title']}: {len(paragraphs)} paragraphs")
# Summary
print(f"\n--- Summary ---")
for seg in SEGMENTS:
slug = seg["slug"]
filepath = os.path.join(output_dir, f"{slug}.json")
if os.path.exists(filepath):
with open(filepath, "r", encoding="utf-8") as f:
doc = json.load(f)
# Show first paragraph preview
first = doc["paragraphs"][0] if doc["paragraphs"] else "(empty)"
# Strip HTML tags for preview
preview = re.sub(r"<[^>]+>", "", first)[:80]
print(f" {seg['order']}. {seg['title']}")
print(f' {len(doc["paragraphs"])} paragraphs — "{preview}..."')
else:
print(f" {seg['order']}. {seg['title']} — NOT EXTRACTED")
if __name__ == "__main__":
main()