Files
spsays-training/scripts/nbs_extract.py
T

809 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Extract Nārada-bhakti-sūtra verses from other.html into structured JSON files."""
import json
import os
import re
import sys
from bs4 import BeautifulSoup, Tag
from indic_transliteration import sanscript
from indic_transliteration.sanscript import transliterate
# --- ScaGoudy → IAST Unicode mapping ---
SCAGOUDY_TO_IAST = {
"ä": "ā",
"Ä": "Ā",
"å": "ṛ",
"Å": "Ṛ",
"ñ": "ṣ",
"Ñ": "Ṣ",
"ö": "ṭ",
"Ö": "Ṭ",
"ë": "ṇ",
"Ë": "Ṇ",
"ò": "ḍ",
"Ò": "Ḍ",
"ç": "ś",
"Ç": "Ś",
"ì": "ṅ",
"ï": "ñ",
"ü": "ū",
"Ü": "Ū",
"é": "ī",
"É": "Ī",
"à": "ṁ",
"ù": "ḥ",
"è": "ṝ",
"û": "ṁl",
"ÿ": "ḷ",
}
# --- CSS class constants ---
BOOK_HEADER_CLASS = "p12" # Book title
SECTION_HEADING_CLASS = (
"p13" # Section bookmarks ("NBS Introduction", "NBS: The Value of Devotion")
)
CHAPTER_NUM_CLASS = "p31" # "Chapter 1" heading
CHAPTER_TITLE_CLASS = "p27" # "The Value of Devotion" heading
BODY_CLASS = "p48" # Body/purport paragraphs (MAIN content class)
VERSE_BOOKMARK_CLASS = "p49" # Verse bookmarks ("NBS 1" through "NBS 8")
VERSE_HEADING_CLASS = "p50" # "SŪTRA N" heading (bold)
DEVANAGARI_SKIP_CLASS_1 = "p220" # Garbled Devanagari in custom font — SKIP
DEVANAGARI_SKIP_CLASS_2 = "p51" # Also garbled Devanagari — SKIP
TEXT_LABEL_CLASS = "p52" # "SŪTRA" label
PADA_TRANSLIT_CLASS = "p5" # Transliteration pādas (italic)
SYNONYMS_HEADER_CLASS = "p165" # "SYNONYMS" header
SYNONYM_TEXT_CLASS = "p221" # Synonyms text (NOTE: p221 for NBS, not p54!)
SYNONYM_TEXT_CLASS_ALT = "p54" # Alternate synonyms class (just in case)
TRANSLATION_LABEL_CLASS = "p55" # "TRANSLATION" / "PURPORT" headers
TRANSLATION_TEXT_CLASS = "p56" # Translation text (bold)
PURPORT_LABEL_ALT_CLASS = "p57" # Alternate "PURPORT" header
QUOTED_VERSE_CLASS = "p53" # Embedded Sanskrit verse in purport
COMMENTATOR_CLASS = "p44" # Verse translation within purport
def decode_scagoudy(text: str) -> str:
"""Convert ScaGoudy-encoded text to proper IAST Unicode."""
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 (artifacts from RTF conversion)
inner = re.sub(
r'<span[^>]*class="[^"]*"[^>]*>(.*?)</span>', r"\1", inner, flags=re.DOTALL
)
# Clean up Apple-tab-span leftovers (tab characters)
inner = inner.replace("\t", " ")
# Normalize whitespace
inner = re.sub(r"\s+", " ", inner).strip()
return inner
def decode_text(tag: Tag) -> str:
"""Extract plain text from a tag and decode ScaGoudy."""
text = tag.get_text()
return decode_scagoudy(text).strip()
def normalize_devanagari_anusvara(text: str) -> str:
"""Replace explicit class nasals before same-class consonants with anusvāra.
Only normalize ङ् and ञ् — these are almost always written with anusvāra.
Keep ण्ड, न्त, म्ब etc. as explicit conjuncts (standard spelling).
"""
replacements = [
# ङ् before velars (क, ख, ग, घ)
(r"ङ्([कखगघ])", r"ं\1"),
# ञ् before palatals (च, छ, ज, झ)
(r"ञ्([चछजझ])", r"ं\1"),
]
for pattern, repl in replacements:
text = re.sub(pattern, repl, text)
return text
def join_virama_words(text: str) -> str:
"""Join Devanagari words where the previous word ends with virāma (्).
Handles both virāma+consonant and virāma+vowel cases.
Uses [ \\t]+ instead of \\s+ to avoid joining across newlines.
"""
# Join virāma + space + consonant (क U+0915 through ह U+0939)
text = re.sub(r"्[ \t]+([क-ह])", r"्\1", text)
# Join virāma + space + independent vowel
_VOWEL_TO_MATRA = {
"अ": "", # inherent vowel — just drop virāma and the अ
"आ": "ा",
"इ": "ि",
"ई": "ी",
"उ": "ु",
"ऊ": "ू",
"ऋ": "ृ",
"ॠ": "ॄ",
"ऌ": "ॢ",
"ॡ": "ॣ",
"ए": "े",
"ऐ": "ै",
"ओ": "ो",
"औ": "ौ",
}
def _virama_vowel_replace(m: re.Match[str]) -> str:
vowel = m.group(1)
return _VOWEL_TO_MATRA.get(vowel, vowel)
text = re.sub(r"्[ \t]+([अ-औॠॡ])", _virama_vowel_replace, text)
return text
def iast_to_devanagari(iast_text: str) -> str:
"""Convert IAST transliteration to Devanagari script."""
lines = iast_text.split("\n")
result_lines = []
for line in lines:
# Remove hyphens between words for Devanagari
clean = line.replace("-", "")
# Normalize ṁ (U+1E41) to ṃ (U+1E43) for proper anusvāra
clean = clean.replace("ṁ", "ṃ")
deva = transliterate(clean, sanscript.IAST, sanscript.DEVANAGARI)
deva = normalize_devanagari_anusvara(deva)
deva = join_virama_words(deva)
result_lines.append(deva)
return "\n".join(result_lines)
def parse_synonyms(text: str) -> list:
"""Parse synonym text into structured array.
Input format: "word—meaning; word—meaning; word—meaning"
"""
result = []
entries = text.split(";")
for entry in entries:
entry = entry.strip()
if not entry:
continue
if "—" in entry:
parts = entry.split("—", 1)
elif "--" in entry:
parts = entry.split("--", 1)
else:
continue
if len(parts) == 2:
word = parts[0].strip()
meaning = parts[1].strip()
# Clean word: remove trailing/leading punctuation
word = word.strip("., ")
if word and meaning:
result.append({"word": word, "meanings": [meaning]})
return result
def pair_padas(padas: list) -> str:
"""Combine pādas into 2-line verse format.
If the number of pādas is odd, the first one is treated as a
standalone line (e.g. speaker attribution like 'uvāca').
Empty strings in the list act as stanza-break sentinels and are
rendered as blank lines in the output.
"""
if not padas:
return ""
# Split pādas into stanza groups separated by "" sentinels
groups: list[list[str]] = [[]]
for p in padas:
if p == "":
groups.append([])
else:
groups[-1].append(p)
result_lines = []
for group in groups:
if not group:
continue
lines = []
i = 0
# If odd number of pādas, first one is standalone (speaker attribution)
if len(group) % 2 == 1:
lines.append(group[0])
i = 1
# Pair remaining pādas
while i < len(group) - 1:
lines.append(group[i] + " " + group[i + 1])
i += 2
# Handle any trailing single pāda
if i < len(group):
lines.append(group[i])
result_lines.append("\n".join(lines))
return "\n\n".join(result_lines)
def get_css_class(tag: Tag) -> str:
"""Get the primary CSS class of a tag."""
classes = tag.get("class", [])
return classes[0] if classes else ""
def is_empty(tag: Tag) -> bool:
"""Check if a tag is empty or whitespace-only."""
text = tag.get_text().strip()
return not text or text == "\xa0"
def write_json(data, filepath):
"""Write JSON with proper Unicode and formatting."""
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}")
# ---------------------------------------------------------------------------
# Boundary detection
# ---------------------------------------------------------------------------
def find_nbs_boundaries(all_paragraphs: list) -> tuple[int, int]:
"""Find the start and end paragraph indices of the Nārada-bhakti-sūtra content.
Start: <p class="p12"> containing "Närada-bhakti-sütra" (ScaGoudy encoded)
End: next <p class="p12"> after start (start of next book)
"""
start_idx = None
end_idx = len(all_paragraphs)
for i, p in enumerate(all_paragraphs):
cls = get_css_class(p)
if cls == BOOK_HEADER_CLASS:
text = p.get_text().strip()
if start_idx is None and (
"rada-bhakti" in text or "Närada" in text or "Narada" in text
):
start_idx = i
continue
# Next p12 after our start → end boundary
if start_idx is not None:
end_idx = i
break
if start_idx is None:
print("ERROR: Could not find 'Nārada-bhakti-sūtra' header in other.html!")
sys.exit(1)
return start_idx, end_idx
def parse_verse_bookmark(text: str) -> int | None:
"""Parse a verse bookmark like 'NBS N'.
Returns sūtra number (1-8), or None.
"""
text = text.strip()
m = re.match(r"NBS\s+(\d+)", text)
if m:
return int(m.group(1))
return None
# ---------------------------------------------------------------------------
# Section boundary detection within NBS range
# ---------------------------------------------------------------------------
def find_section_boundaries(all_paragraphs: list, start_idx: int, end_idx: int) -> dict:
"""Find key section boundaries within the NBS content.
Returns dict with section names and their start indices.
"""
sections = {}
for i in range(start_idx, end_idx):
p = all_paragraphs[i]
cls = get_css_class(p)
text = p.get_text().strip()
# Introduction bookmark (p13 with "NBS Introduction")
if cls == SECTION_HEADING_CLASS and "NBS Introduction" in text:
sections["introduction_bookmark"] = i
# Chapter bookmark (p13 with "NBS: The Value of Devotion") — note but skip
elif cls == SECTION_HEADING_CLASS and "NBS:" in text and "Value" in text:
sections["chapter_bookmark"] = i
# First verse bookmark (NBS 1)
elif cls == VERSE_BOOKMARK_CLASS:
decoded = decode_text(p)
vnum = parse_verse_bookmark(decoded)
if vnum == 1 and "first_verse" not in sections:
sections["first_verse"] = i
break
return sections
# ---------------------------------------------------------------------------
# Intro segment extraction
# ---------------------------------------------------------------------------
def extract_intro_segment(
all_paragraphs: list, start_idx: int, end_idx: int
) -> list[str]:
"""Extract paragraphs for the introduction segment.
Decodes ScaGoudy, preserves HTML formatting, skips empty paragraphs.
Includes p48 (body) content and other content classes.
"""
result = []
for i in range(start_idx, end_idx):
p = all_paragraphs[i]
cls = get_css_class(p)
if is_empty(p):
continue
# Skip bookmark and header classes
if cls in (
SECTION_HEADING_CLASS,
BOOK_HEADER_CLASS,
CHAPTER_NUM_CLASS,
CHAPTER_TITLE_CLASS,
):
continue
# Include body paragraphs
if cls in (BODY_CLASS,):
html = decode_html_content(p)
if html:
result.append(html)
# Include quoted verses in introduction
elif cls == QUOTED_VERSE_CLASS:
html = decode_html_content(p)
if html:
result.append(f'<div class="quoted-verse">{html}</div>')
# Include verse translations in introduction
elif cls == COMMENTATOR_CLASS:
html = decode_html_content(p)
if html:
result.append(html)
return result
# ---------------------------------------------------------------------------
# Verse extraction state machine
# ---------------------------------------------------------------------------
def extract_verses(all_paragraphs: list, verses_start: int, verses_end: int) -> list:
"""Extract verses from the NBS content using a state machine.
States: IDLE → VERSE_START → TRANSLITERATION → SYNONYMS →
TRANSLATION → POST_TRANSLATION → PURPORT → (back to IDLE)
Returns list of verse dicts.
"""
verses = []
current_verse = None
state = "IDLE"
for i in range(verses_start, verses_end):
p = all_paragraphs[i]
cls = get_css_class(p)
# --- Verse bookmark (p49) ---
if cls == VERSE_BOOKMARK_CLASS:
# Finalize previous verse if any
if current_verse and state != "IDLE":
_finalize_verse(current_verse)
verses.append(current_verse)
text = decode_text(p)
verse_num = parse_verse_bookmark(text)
if verse_num is not None:
reference = f"NBS {verse_num}"
current_verse = {
"book_title": "Nārada-bhakti-sūtra",
"chapter": 1,
"verse": verse_num,
"reference": reference,
"devanagari": "",
"transliteration": "",
"_padas": [],
"synonyms": [],
"translation": "",
"purport": [],
}
state = "VERSE_START"
continue
if not current_verse:
continue
# --- Skip garbled Devanagari lines (p220, p51) ---
if cls in (DEVANAGARI_SKIP_CLASS_1, DEVANAGARI_SKIP_CLASS_2):
continue
# --- Verse heading (p50 — "SŪTRA N") - confirms sūtra number ---
if cls == VERSE_HEADING_CLASS:
inner_text = p.get_text().strip().upper()
# This is the "SŪTRA N" heading — skip it
if state == "VERSE_START":
continue
# --- TEXT/SŪTRA label (p52) - transition to transliteration ---
if cls == TEXT_LABEL_CLASS:
state = "TRANSLITERATION"
continue
# --- Pāda transliteration lines (p5) ---
if cls == PADA_TRANSLIT_CLASS and state in ("VERSE_START", "TRANSLITERATION"):
state = "TRANSLITERATION"
line = decode_text(p)
# Remove trailing period
line = line.rstrip(".")
line = line.strip()
if line:
current_verse["_padas"].append(line)
continue
# --- SYNONYMS header (p165) ---
if cls == SYNONYMS_HEADER_CLASS:
state = "SYNONYMS"
continue
# --- Synonyms text (p221 for NBS, also check p54 as fallback) ---
if cls in (SYNONYM_TEXT_CLASS, SYNONYM_TEXT_CLASS_ALT) and current_verse:
state = "SYNONYMS"
syn_text = decode_text(p)
syn_text = syn_text.strip()
parsed = parse_synonyms(syn_text)
current_verse["synonyms"].extend(parsed)
continue
# --- TRANSLATION label (p55) — check text content to distinguish from PURPORT ---
if cls == TRANSLATION_LABEL_CLASS and current_verse:
inner_text = p.get_text().strip().upper()
if "TRANSLATION" in inner_text:
state = "TRANSLATION"
elif "PURPORT" in inner_text:
state = "PURPORT"
continue
# --- PURPORT label via p57 (alt class) ---
if cls == PURPORT_LABEL_ALT_CLASS and current_verse:
inner_text = p.get_text().strip().upper()
if "PURPORT" in inner_text:
state = "PURPORT"
continue
elif "TRANSLATION" in inner_text:
state = "TRANSLATION"
continue
# --- Translation text (p56) ---
if cls == TRANSLATION_TEXT_CLASS and current_verse and state == "TRANSLATION":
html = decode_html_content(p)
# Remove bold wrapper if present (the whole translation is bold)
html = re.sub(r"^<b>(.*)</b>$", r"\1", html, flags=re.DOTALL)
html = html.strip()
current_verse["translation"] = html
state = "POST_TRANSLATION"
continue
# --- Purport paragraphs (p48) ---
if (
cls == BODY_CLASS
and current_verse
and state in ("PURPORT", "POST_TRANSLATION")
):
state = "PURPORT"
html = decode_html_content(p)
if html:
current_verse["purport"].append(html)
continue
# --- Embedded Sanskrit verse in purport (p53) ---
if cls == QUOTED_VERSE_CLASS and current_verse and state == "PURPORT":
html = decode_html_content(p)
if html:
# Wrap as centered italic div for display
current_verse["purport"].append(
f'<div class="quoted-verse">{html}</div>'
)
continue
# --- Verse translation within purport (p44) ---
if cls == COMMENTATOR_CLASS and current_verse and state == "PURPORT":
html = decode_html_content(p)
if html:
current_verse["purport"].append(html)
continue
# Finalize last verse
if current_verse and state != "IDLE":
_finalize_verse(current_verse)
verses.append(current_verse)
return verses
def _finalize_verse(verse: dict):
"""Build transliteration and Devanagari from pādas. Remove internal fields."""
padas = verse.get("_padas", [])
# Transliteration: one pāda per line, blank sentinels become blank lines
trans_lines = []
for p in padas:
if p == "":
trans_lines.append("") # blank line between stanza groups
else:
trans_lines.append(p)
verse["transliteration"] = "\n".join(trans_lines)
# Devanagari: pair pādas into 2-line verse structure, then convert
if padas:
paired = pair_padas(padas)
verse["devanagari"] = iast_to_devanagari(paired)
# Clean up internal field
if "_padas" in verse:
del verse["_padas"]
# ---------------------------------------------------------------------------
# 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_base = os.path.join(base_dir, "lib", "jsons", "books", "nbs")
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")
all_paragraphs = soup.find_all("p")
print(f"Total paragraphs in file: {len(all_paragraphs)}")
# --- Find NBS boundaries ---
nbs_start, nbs_end = find_nbs_boundaries(all_paragraphs)
print(f"NBS content: paragraphs {nbs_start} to {nbs_end}")
# --- Find section boundaries ---
sections = find_section_boundaries(all_paragraphs, nbs_start, nbs_end)
print(f"\nSection boundaries found:")
for name, idx in sorted(sections.items(), key=lambda x: x[1]):
print(f" [{idx}] {name}")
intro_bm = sections.get("introduction_bookmark")
first_verse = sections.get("first_verse")
if first_verse is None:
print("ERROR: Could not find first verse bookmark (NBS 1)!")
sys.exit(1)
# --- Extract intro segment ---
print("\n--- Extracting Intro Segment ---")
intro_segments = []
if intro_bm is not None:
intro_end = first_verse
intro_paras = extract_intro_segment(all_paragraphs, intro_bm, intro_end)
intro_segment = {
"slug": "introduction",
"title": "Introduction",
"type": "intro-segment",
"book": "nbs",
"order": 1,
"paragraphs": intro_paras,
}
intro_segments.append(intro_segment)
print(f" Introduction: {len(intro_paras)} paragraphs")
# --- Extract verses ---
print("\n--- Extracting Verses ---")
verses = extract_verses(all_paragraphs, first_verse, nbs_end)
print(f"Extracted {len(verses)} verses (expected 8: Sūtras 1-8)")
# --- Write intro.json ---
intro_meta = {
"book": "nbs",
"title": "Nārada-bhakti-sūtra",
"author": "His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda",
"year": 1989,
"description": "The aphorisms of devotional service by Nārada Muni — translations and purports for sūtras 1–8.",
"totalChapters": 1,
"chapters": [
{
"number": 1,
"title": "The Value of Devotion",
"totalVerses": 8,
}
],
}
write_json(intro_meta, os.path.join(output_base, "intro.json"))
# --- Write intro segments ---
print("\n--- Writing Intro Segments ---")
for seg in intro_segments:
filepath = os.path.join(output_base, "intro", f"{seg['slug']}.json")
write_json(seg, filepath)
# --- Write verse JSON files ---
print("\n--- Writing Verse Files ---")
for verse in verses:
text_num = verse["verse"]
filename = f"{str(text_num).zfill(2)}.json"
filepath = os.path.join(output_base, "chapters", "01", "verses", filename)
write_json(verse, filepath)
# --- Summary ---
print(f"\n{'=' * 60}")
print(f" EXTRACTION SUMMARY")
print(f"{'=' * 60}")
print(f" Book: Nārada-bhakti-sūtra")
print(f" Intro segments: {len(intro_segments)}")
for seg in intro_segments:
print(
f" {seg['order']}. {seg['title']}: {len(seg['paragraphs'])} paragraphs"
)
print(f" Verses extracted: {len(verses)}")
texts_found = sorted([v["verse"] for v in verses])
if texts_found:
print(
f" Sūtras: {texts_found[0]}-{texts_found[-1]} ({len(texts_found)} total)"
)
else:
print(" Sūtras: None")
total_purport_paras = sum(len(v["purport"]) for v in verses)
total_synonyms = sum(len(v["synonyms"]) for v in verses)
print(f" Total purport ¶s: {total_purport_paras}")
print(f" Total synonyms: {total_synonyms}")
print(f" Output directory: {output_base}/")
# --- Quality checks ---
print(f"\n--- Quality Checks ---")
# Check all verses have expected fields
issues = []
for verse in verses:
ref = verse["reference"]
if not verse["transliteration"]:
issues.append(f" {ref}: missing transliteration")
if not verse["devanagari"]:
issues.append(f" {ref}: missing devanagari")
if not verse["synonyms"]:
issues.append(f" {ref}: missing synonyms")
if not verse["translation"]:
issues.append(f" {ref}: missing translation")
if not verse["purport"]:
issues.append(f" {ref}: missing purport (may be intentional)")
if issues:
print(" ISSUES FOUND:")
for issue in issues:
print(issue)
else:
print(
" All verses have transliteration, devanagari, synonyms, and translation ✓"
)
# Check expected verse count
expected_texts = set(range(1, 9))
found_texts = set(v["verse"] for v in verses)
missing = expected_texts - found_texts
extra = found_texts - expected_texts
if missing:
print(f" MISSING sūtras: {sorted(missing)}")
if extra:
print(f" UNEXPECTED sūtras: {sorted(extra)}")
if not missing and not extra:
print(f" All 8 sūtras found ✓")
# --- ScaGoudy spot checks ---
print(f"\n--- ScaGoudy Spot Checks ---")
for verse in verses:
ref = verse["reference"]
trans = verse["transliteration"]
# Check for leftover ScaGoudy characters
leftover = set()
for ch in trans:
if ch in SCAGOUDY_TO_IAST:
leftover.add(ch)
if leftover:
print(
f" WARNING: {ref} transliteration has leftover ScaGoudy chars: {leftover}"
)
else:
print(f" {ref}: transliteration clean ✓")
# --- Samples ---
print(f"\n--- Sample: Sūtra 1 (first) ---")
t1 = [v for v in verses if v["verse"] == 1]
if t1:
v = t1[0]
print(f" Reference: {v['reference']}")
print(f" Transliteration: {v['transliteration'][:100]}...")
print(f" Devanagari: {v['devanagari'][:100]}...")
print(f" Synonyms: {len(v['synonyms'])} entries")
if v["synonyms"]:
print(f" First: {v['synonyms'][0]}")
print(f" Last: {v['synonyms'][-1]}")
trans_preview = re.sub(r"<[^>]+>", "", v["translation"])[:120]
print(f" Translation: {trans_preview}...")
print(f" Purport ¶s: {len(v['purport'])}")
print(f"\n--- Sample: Sūtra 4 (middle) ---")
t4 = [v for v in verses if v["verse"] == 4]
if t4:
v = t4[0]
print(f" Reference: {v['reference']}")
print(f" Transliteration: {v['transliteration'][:100]}...")
print(f" Devanagari: {v['devanagari'][:100]}...")
print(f" Synonyms: {len(v['synonyms'])} entries")
if v["synonyms"]:
print(f" First: {v['synonyms'][0]}")
print(f" Last: {v['synonyms'][-1]}")
trans_preview = re.sub(r"<[^>]+>", "", v["translation"])[:120]
print(f" Translation: {trans_preview}...")
print(f" Purport ¶s: {len(v['purport'])}")
print(f"\n--- Sample: Sūtra 8 (last) ---")
t8 = [v for v in verses if v["verse"] == 8]
if t8:
v = t8[0]
print(f" Reference: {v['reference']}")
print(f" Transliteration: {v['transliteration'][:100]}...")
print(f" Devanagari: {v['devanagari'][:100]}...")
print(f" Synonyms: {len(v['synonyms'])} entries")
if v["synonyms"]:
print(f" First: {v['synonyms'][0]}")
print(f" Last: {v['synonyms'][-1]}")
trans_preview = re.sub(r"<[^>]+>", "", v["translation"])[:120]
print(f" Translation: {trans_preview}...")
print(f" Purport ¶s: {len(v['purport'])}")
# --- Intro segment details ---
print(f"\n--- Intro Segment Details ---")
for seg in intro_segments:
print(f" {seg['title']}: {len(seg['paragraphs'])} paragraphs")
if seg["paragraphs"]:
first_preview = re.sub(r"<[^>]+>", "", seg["paragraphs"][0])[:120]
print(f" First ¶: {first_preview}...")
last_preview = re.sub(r"<[^>]+>", "", seg["paragraphs"][-1])[:120]
print(f" Last ¶: {last_preview}...")
if __name__ == "__main__":
main()