#!/usr/bin/env python3 """Extract Prayers of King Kulaśekhara (1958) from others-before-1977.html. The book has 6 chapters (one per verse/prayer): 1. Prayer 1 — "Śrī Vallabha" (prayer to Mukunda) 2. Prayer 2 — "Jayatu Jayatu" (glories to Devakī-nandana) 3. Prayer 3 — "Mukunda Mūrdhnā" (prayer for remembrance) 4. Prayer 4 — "Nāhaṁ Vande" (neither heaven nor hell) 5. Prayer 5 — "Nāsthā Dharme" (unflinching devotion) 6. Prayer 6 — "Divi Vā Bhuvi" (remembrance at death) Output: lib/jsons/books/pk/intro.json — Book metadata lib/jsons/books/pk/chapters/*.json — Chapters (01–06) Usage: python3 scripts/pk_extract.py """ import json import os import re import sys from bs4 import BeautifulSoup, Tag # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def decode_html_content(tag: Tag) -> str: """Extract inner HTML from a tag, preserve formatting tags, strip spans.""" inner = "".join(str(child) for child in tag.children) # Remove span tags with CSS classes (RTF conversion artifacts) inner = re.sub( r']*class="[^"]*"[^>]*>(.*?)', 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: """Get plain text from a tag.""" return tag.get_text().strip() def get_css_class(tag: Tag) -> str: """Get the first CSS class of a tag, or empty string.""" 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" # --------------------------------------------------------------------------- # CSS class categorization for PK # --------------------------------------------------------------------------- # Document header DOC_HEADER_CLASS = "p4" # "Prayers of King Kulaçekhara 1958 [Handwritten]" TITLE_CLASS = "p115" # Bold title # Chapter bookmark class CHAPTER_BOOKMARK_CLASS = "p17" # "Prayers of King Kulaçekhara 1950: N" # Verse number markers — skip VERSE_MARKER_CLASSES = frozenset(["p108", "p9"]) # Devanagari marker — skip DEVANAGARI_MARKER_CLASS = "p36" # Section header class (Wordings., Synonyms., Translation., Purport.) SECTION_HEADER_CLASSES = frozenset(["p20", "p121"]) # Body paragraph classes BODY_CLASSES = frozenset(["p6", "p34", "p38", "p71"]) # Page number class — skip PAGE_NUMBER_CLASSES = frozenset(["p15", "p41", "p124"]) # Empty/break classes — skip SKIP_CLASSES = frozenset(["p24", "p94", "p119", "p122", "p123", "p120"]) # Next work boundary NEXT_WORK_BOUNDARY_CLASS = "p125" # --------------------------------------------------------------------------- # Boundary detection # --------------------------------------------------------------------------- def find_pk_boundaries(all_elements: list) -> tuple: """Find the start and end element indices of the PK content. Start:

containing "Prayers of King Kula" End:

that starts the next work ("Shaktipujah") """ start_idx = None end_idx = len(all_elements) for i, el in enumerate(all_elements): if el.name != "p": continue cls = get_css_class(el) text = decode_text(el) # Find start: p4 containing "Prayers of King" if start_idx is None: if cls == DOC_HEADER_CLASS and "Prayers of King" in text: start_idx = i continue continue # Find end: p125 that starts a different work if cls == NEXT_WORK_BOUNDARY_CLASS: end_idx = i break if start_idx is None: print( "ERROR: Could not find 'Prayers of King Kulaçekhara' in others-before-1977.html!" ) sys.exit(1) return start_idx, end_idx def find_chapter_bookmarks(all_elements: list, start_idx: int, end_idx: int) -> list: """Find all PK chapter bookmark tags (p17) in the PK range. Returns list of (element_index, verse_number). """ bookmarks = [] for i in range(start_idx, end_idx): el = all_elements[i] if el.name != "p": continue cls = get_css_class(el) if cls == CHAPTER_BOOKMARK_CLASS: text = decode_text(el).strip() if "Prayers of King" in text or "Kulaçekhara" in text.replace("ç", "ç"): # Extract the verse number from the end match = re.search(r":\s*(\d+)\s*$", text) if match: verse_num = int(match.group(1)) bookmarks.append((i, verse_num)) return bookmarks # --------------------------------------------------------------------------- # Table synonym extraction # --------------------------------------------------------------------------- def extract_synonyms_from_table(table: Tag) -> str: """Extract synonyms from a and convert to text. Each cell contains a

with "word=meaning" format. Convert to "word—meaning; word—meaning; ..." format. """ entries = [] for row in table.find_all("tr"): for cell in row.find_all("td"): # Get all

tags inside the cell for p in cell.find_all("p"): text = decode_text(p).strip() if not text or text == "\xa0": continue # Must contain '=' to be a synonym entry if "=" not in text: continue # Replace = with em-dash text = text.replace("=", "—", 1) # Clean trailing whitespace/period if needed text = text.strip() entries.append(text) if not entries: return "" return "; ".join(entries) # --------------------------------------------------------------------------- # Paragraph extraction for a chapter # --------------------------------------------------------------------------- def extract_chapter_paragraphs( all_elements: list, start_idx: int, end_idx: int, ) -> list: """Extract paragraphs for a chapter (verse). Handles section headers (Wordings., Synonyms., Translation., Purport.), body paragraphs, and synonym tables. Returns a list of HTML paragraph strings. """ result = [] # Build a set of

tags that are inside tables, so we don't double-process table_p_tags = set() for i in range(start_idx, end_idx): el = all_elements[i] if el.name == "table": for p in el.find_all("p"): table_p_tags.add(id(p)) for i in range(start_idx, end_idx): el = all_elements[i] # Handle tables (synonyms) if el.name == "table": table_cls = get_css_class(el) if table_cls == "t1": synonyms_text = extract_synonyms_from_table(el) if synonyms_text: result.append(synonyms_text) continue # Only process

tags from here if el.name != "p": continue # Skip

tags that are inside tables (already processed) if id(el) in table_p_tags: continue cls = get_css_class(el) # Skip empty paragraphs if is_empty(el): continue # Skip page numbers if cls in PAGE_NUMBER_CLASSES: continue # Skip verse markers if cls in VERSE_MARKER_CLASSES: text = decode_text(el) # p9 can also be "(Devanägaré)" or prayer number markers — skip all continue # Skip devanagari markers if cls == DEVANAGARI_MARKER_CLASS: continue # Skip chapter bookmarks if cls == CHAPTER_BOOKMARK_CLASS: continue # Skip document header and title (only at very start) if cls == DOC_HEADER_CLASS: continue if cls == TITLE_CLASS: continue # Skip empty/break classes if cls in SKIP_CLASSES: continue # Section headers: Wordings., Synonyms., Translation., Purport. if cls in SECTION_HEADER_CLASSES: text = decode_text(el).strip() # Normalize section header names text_lower = text.lower().rstrip(".") if "wording" in text_lower: result.append("Wordings") elif ( "synonym" in text_lower or "synomym" in text_lower or "synonysm" in text_lower ): result.append("Synonyms") elif "translation" in text_lower: result.append("Translation") elif "purport" in text_lower: result.append("Purport") else: # Unknown header — include as bold html = decode_html_content(el) if html: result.append(f"{html}") continue # Body paragraphs if cls in BODY_CLASSES: html = decode_html_content(el) if html: result.append(html) continue # Fallback: try to extract content from unknown classes html = decode_html_content(el) if html: result.append(html) return result # --------------------------------------------------------------------------- # Main extraction logic # --------------------------------------------------------------------------- def extract_pk(soup: BeautifulSoup) -> dict: """Extract the entire Prayers of King Kulaśekhara from the parsed HTML. Returns a dict with: "chapters": list of chapter dicts "chapter_listing": list of {number, title} for intro.json """ # We need both

and

elements in document order all_elements = list(soup.find_all(["p", "table"])) # Step 1: Find PK boundaries pk_start, pk_end = find_pk_boundaries(all_elements) print(f" PK content: elements {pk_start} to {pk_end}") print(f" Total elements in range: {pk_end - pk_start}") # Step 2: Find all chapter bookmarks (p17) bookmarks = find_chapter_bookmarks(all_elements, pk_start, pk_end) print(f" Found {len(bookmarks)} PK chapter bookmarks:") for pos, verse_num in bookmarks: print(f" Element {pos}: Verse {verse_num}") # If we didn't find 6 bookmarks, also look for the first verse that starts # before any p17 bookmark (verse 1 may start with p108 [1]) # Actually, verse 1's bookmark IS in the HTML. Let's verify. if len(bookmarks) < 6: print(f" WARNING: Expected 6 bookmarks, found {len(bookmarks)}") # Step 3: Build chapter ranges # Each bookmark marks the START of a verse section. # Content for verse N runs from bookmark N to bookmark N+1 (or pk_end). chapters = [] for idx, (bk_pos, verse_num) in enumerate(bookmarks): # Content starts after the bookmark content_start = bk_pos + 1 # Content ends at next bookmark or pk_end if idx + 1 < len(bookmarks): content_end = bookmarks[idx + 1][0] else: content_end = pk_end paragraphs = extract_chapter_paragraphs( all_elements, content_start, content_end ) ch = { "book": "pk", "chapter": verse_num, "title": f"Prayer {verse_num}", "type": "chapter", "paragraphs": paragraphs, } chapters.append(ch) # Step 4: Build chapter listing for intro.json chapter_listing = [ {"number": ch["chapter"], "title": ch["title"]} for ch in chapters ] return { "chapters": chapters, "chapter_listing": chapter_listing, } # --------------------------------------------------------------------------- # Output # --------------------------------------------------------------------------- def write_json(data, filepath): """Write data as formatted JSON to filepath, creating directories as needed.""" 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) def main(): base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) html_path = os.path.join(base_dir, "public", "html", "others-before-1977.html") output_base = os.path.join(base_dir, "lib", "jsons", "books", "pk") 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("Extracting Prayers of King Kulaśekhara (1958)...\n") result = extract_pk(soup) chapters = result["chapters"] chapter_listing = result["chapter_listing"] # --- Write intro.json --- intro_meta = { "book": "pk", "title": "Prayers of King Kulaśekhara (1958)", "author": "His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda", "year": 1958, "description": "Early handwritten translations and purports of the Mukunda-mālā-stotra prayers (verses 1–6).", "totalChapters": 6, "chapters": chapter_listing, } intro_path = os.path.join(output_base, "intro.json") write_json(intro_meta, intro_path) print(f"\n Written: {intro_path}") # --- Write chapters --- print(f"\n--- Chapters ---") for ch in chapters: ch_num_str = str(ch["chapter"]).zfill(2) filepath = os.path.join(output_base, "chapters", f"{ch_num_str}.json") write_json(ch, filepath) print( f" Ch {ch['chapter']:>2}: {len(ch['paragraphs']):>3} paragraphs — " f"{ch['title']}" ) # --- Summary --- total_chapter_paras = sum(len(ch["paragraphs"]) for ch in chapters) print(f"\n{'=' * 60}") print(f" EXTRACTION SUMMARY") print(f"{'=' * 60}") print(f" Chapters: {len(chapters)}") for ch in chapters: print( f" Ch {ch['chapter']}: {len(ch['paragraphs'])} paragraphs — {ch['title']}" ) print(f" Total paragraphs: {total_chapter_paras}") if chapters: print(f" Avg paras/chapter: {total_chapter_paras / len(chapters):.1f}") print(f" Output directory: {output_base}/") # --- Samples --- for ch_idx, ch in enumerate(chapters): print(f"\n--- Sample: Chapter {ch['chapter']} ({ch['title']}) ---") for i, p in enumerate(ch["paragraphs"][:5]): preview = re.sub(r"<[^>]+>", "", p)[:120] print(f" [{i}] {preview}") if len(ch["paragraphs"]) > 5: print(f" ... ({len(ch['paragraphs'])} total paragraphs)") # --- Quality Checks --- print(f"\n--- Quality Checks ---") # Check all 6 chapters present expected_chapters = set(range(1, 7)) actual_chapters = set(ch["chapter"] for ch in chapters) missing = expected_chapters - actual_chapters extra = actual_chapters - expected_chapters if missing: print(f" ⚠ Missing chapters: {sorted(missing)}") if extra: print(f" ⚠ Extra chapters: {sorted(extra)}") if not missing and not extra: print(f" ✅ All 6 chapters present") # Check empty chapters empty_chapters = [ch["chapter"] for ch in chapters if len(ch["paragraphs"]) == 0] if empty_chapters: print(f" ⚠ Empty chapters (0 paragraphs): {empty_chapters}") else: print(f" ✅ No empty chapters") # Check each chapter has expected sections for ch in chapters: ch_text = " ".join(ch["paragraphs"]) has_wordings = "Wordings" in ch_text has_synonyms = "Synonyms" in ch_text has_translation = "Translation" in ch_text has_purport = "Purport" in ch_text sections = [] if has_wordings: sections.append("W") if has_synonyms: sections.append("S") if has_translation: sections.append("T") if has_purport: sections.append("P") status = "✅" if len(sections) == 4 else "⚠" print(f" {status} Ch {ch['chapter']}: sections [{', '.join(sections)}]") # Check synonym entries use em-dash format synonym_count = 0 for ch in chapters: for p in ch["paragraphs"]: if "—" in p and "" not in p: synonym_count += 1 break print(f" ℹ Chapters with em-dash synonyms: {synonym_count}/6") # Verify no Shaktipujah content leaked in all_text = "" for ch in chapters: all_text += " ".join(ch["paragraphs"]) if "Shaktipujah" in all_text or "Baidya Nath" in all_text: print(f" ⚠ Content from Shaktipujah may have leaked into PK!") else: print(f" ✅ No content leak from next work detected") # Check last chapter ends cleanly if chapters: last_ch = chapters[-1] last_para = last_ch["paragraphs"][-1] if last_ch["paragraphs"] else "" last_preview = re.sub(r"<[^>]+>", "", last_para)[:80] print(f' ℹ Last paragraph of Ch {last_ch["chapter"]}: "{last_preview}..."') print(f"\nDone! ✨") if __name__ == "__main__": main()