#!/usr/bin/env python3 """Extract five "Essays on Śrī Caitanya-caritāmṛta" from others-before-1977.html. Essay 1: "The Spiritual Masters" (cc-essays, order 1, 1960) Essay 2: "The Absolute Truth" (cc-essays, order 2, 1957) Essay 3: "The Causes of Lord Caitanya's Incarnation" (cc-essays, order 3, 1957) Essay 4: "The Confidential Reasons for Lord Caitanya's Appearance" (cc-essays, order 4, 1957) Essay 5: "The Glories of Lord Nityānanda" (cc-essays, order 5, 1957) All five are plain English (NO ScaGoudy decoding needed). Output: lib/jsons/essays/the-spiritual-masters.json lib/jsons/essays/the-absolute-truth.json lib/jsons/essays/causes-of-lord-caitanyas-incarnation.json lib/jsons/essays/confidential-reasons-for-lord-caitanyas-appearance.json lib/jsons/essays/glories-of-lord-nityananda.json Usage: python3 scripts/cc_essays_extract.py """ import json import os import re import sys from bs4 import BeautifulSoup, Tag # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- 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" def clean_html_content(tag: Tag) -> str: """Extract inner HTML from a tag, strip span class artifacts, preserve , ,
.""" inner = "".join(str(child) for child in tag.children) # Remove span tags with CSS classes (RTF conversion artifacts) but keep their content inner = re.sub( r']*class="[^"]*"[^>]*>(.*?)', r"\1", inner, flags=re.DOTALL ) # Remove any remaining bare or tags without classes inner = re.sub(r"]*>", "", inner) # Clean up Apple-tab-span leftovers (tab characters) inner = inner.replace("\t", " ") # Normalize whitespace (but preserve
tags) inner = re.sub(r"\s+", " ", inner).strip() return inner 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 is_divider(tag: Tag) -> bool: """Check if a p9 tag is just a divider line (underscores) or a page header.""" text = tag.get_text().strip() # Dividers are lines of underscores if re.match(r"^_+$", text): return True return False def is_page_header(tag: Tag) -> bool: """Check if a p9 tag is a page header like 'Essays on Shri Chaitanya Charitamrita Page 30'.""" text = tag.get_text().strip() if re.search(r"Page\s+\d+", text): return True return False def is_editor_note(tag: Tag) -> bool: """Check if a paragraph is an editor note like '[This file has been proofread...]'.""" text = tag.get_text().strip() if text.startswith("[") and ( "proofread" in text.lower() or "—Ed" in text or "- Ed" in text or ".—Ed" in text ): return True return False # --------------------------------------------------------------------------- # Essay configurations # --------------------------------------------------------------------------- AUTHOR = "His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda" ESSAYS = [ { "slug": "the-spiritual-masters", "title": "The Spiritual Masters", "author": AUTHOR, "year": 1960, "group": "cc-essays", "order": 1, }, { "slug": "the-absolute-truth", "title": "The Absolute Truth", "author": AUTHOR, "year": 1957, "group": "cc-essays", "order": 2, }, { "slug": "causes-of-lord-caitanyas-incarnation", "title": "The Causes of Lord Caitanya's Incarnation", "author": AUTHOR, "year": 1957, "group": "cc-essays", "order": 3, }, { "slug": "confidential-reasons-for-lord-caitanyas-appearance", "title": "The Confidential Reasons for Lord Caitanya's Appearance", "author": AUTHOR, "year": 1957, "group": "cc-essays", "order": 4, }, { "slug": "glories-of-lord-nityananda", "title": "The Glories of Lord Nityānanda", "author": AUTHOR, "year": 1957, "group": "cc-essays", "order": 5, }, ] # --------------------------------------------------------------------------- # Extraction logic # --------------------------------------------------------------------------- def parse_all_paragraphs(html_path: str) -> list[Tag]: """Parse the entire HTML and return all

tags.""" with open(html_path, "r", encoding="utf-8") as f: content = f.read() soup = BeautifulSoup(content, "html.parser") return list(soup.find_all("p")) def find_tag_index(tags: list[Tag], start_from: int, predicate) -> int | None: """Find index of first tag matching predicate, starting from start_from.""" for i in range(start_from, len(tags)): if predicate(tags[i]): return i return None def extract_body_paragraphs( tags: list[Tag], start_idx: int, end_idx: int, include_p9_content: bool = True ) -> list[str]: """Extract body paragraphs from tags[start_idx] to tags[end_idx] (exclusive). Includes p6 paragraphs (main body) and optionally p9 paragraphs that contain actual content (Sanskrit verses, section markers) but skips p9 paragraphs that are dividers or page headers. """ paragraphs = [] body_classes = {"p6"} if include_p9_content: body_classes.add("p9") for i in range(start_idx, end_idx): tag = tags[i] css = get_css_class(tag) if css not in body_classes: continue if is_empty(tag): continue # For p9 tags, skip dividers and page headers if css == "p9": if is_divider(tag) or is_page_header(tag): continue html = clean_html_content(tag) if html and html != " ": paragraphs.append(html) return paragraphs def extract_body_paragraphs_extended( tags: list[Tag], start_idx: int, end_idx: int, body_classes: set[str] | None = None, skip_editor_notes: bool = False, ) -> list[str]: """Extract body paragraphs from tags[start_idx] to tags[end_idx] (exclusive). Like extract_body_paragraphs but with configurable body classes and optional editor-note skipping. """ if body_classes is None: body_classes = {"p6"} paragraphs = [] for i in range(start_idx, end_idx): tag = tags[i] css = get_css_class(tag) if css not in body_classes: continue if is_empty(tag): continue # For p9 tags, skip dividers and page headers if css == "p9": if is_divider(tag) or is_page_header(tag): continue # Skip editor notes if requested if skip_editor_notes and is_editor_note(tag): continue html = clean_html_content(tag) if html and html != " ": paragraphs.append(html) return paragraphs def extract_essay_1(tags: list[Tag]) -> dict: """Extract 'The Spiritual Masters' essay. Start:

(The Spiritual Masters).

Body: p6 and p9 paragraphs after start End: Next

(contains [BTG 1960]) """ # Find the title tag start_idx = find_tag_index( tags, 0, lambda t: get_css_class(t) == "p8" and "Spiritual Masters" in t.get_text(), ) if start_idx is None: raise ValueError("Could not find Essay 1 start marker (p8 'Spiritual Masters')") # Find the end marker: next p10 end_idx = find_tag_index(tags, start_idx + 1, lambda t: get_css_class(t) == "p10") if end_idx is None: raise ValueError("Could not find Essay 1 end marker (p10)") print(f" Essay 1: tags[{start_idx}] → tags[{end_idx}] (exclusive)") # Body starts after the p8 title tag paragraphs = extract_body_paragraphs(tags, start_idx + 1, end_idx) essay = dict(ESSAYS[0]) essay["paragraphs"] = paragraphs return essay def extract_essay_2(tags: list[Tag]) -> dict: """Extract 'The Absolute Truth' essay. Start:

(Second Part)

Body: p6 paragraphs after start End: Next

after start """ # Find the start marker start_idx = find_tag_index( tags, 0, lambda t: get_css_class(t) == "p9" and "(Second Part)" in t.get_text(), ) if start_idx is None: raise ValueError("Could not find Essay 2 start marker (p9 '(Second Part)')") # Find the end marker: next p11 end_idx = find_tag_index(tags, start_idx + 1, lambda t: get_css_class(t) == "p11") if end_idx is None: raise ValueError("Could not find Essay 2 end marker (p11)") print(f" Essay 2: tags[{start_idx}] → tags[{end_idx}] (exclusive)") # Body starts after the "(Second Part)" header paragraphs = extract_body_paragraphs(tags, start_idx + 1, end_idx) essay = dict(ESSAYS[1]) essay["paragraphs"] = paragraphs return essay def extract_essay_3(tags: list[Tag]) -> dict: """Extract 'The Causes of Lord Caitanya's Incarnation' essay. Start:

containing "Essays on Shri Chaitanya Charitamrita. III" Body: p6 paragraphs after start End: Next

after start. The

____

divider is skipped. But the

Thus ends the III part...

IS included. """ # Find the start marker — note the ". III" with period start_idx = find_tag_index( tags, 0, lambda t: ( get_css_class(t) == "p9" and "Essays on Shri Chaitanya Charitamrita. III" in t.get_text() ), ) if start_idx is None: raise ValueError( "Could not find Essay 3 start marker (p9 'Essays on Shri Chaitanya Charitamrita. III')" ) # Find the end marker: next p11 end_idx = find_tag_index(tags, start_idx + 1, lambda t: get_css_class(t) == "p11") if end_idx is None: raise ValueError("Could not find Essay 3 end marker (p11)") print(f" Essay 3: tags[{start_idx}] → tags[{end_idx}] (exclusive)") # Body starts after the header tag. # We collect p6 paragraphs. The divider (p9 with underscores) is skipped # automatically by extract_body_paragraphs. The "Thus ends the III part..." # paragraph is a p6, so it will be included. paragraphs = extract_body_paragraphs(tags, start_idx + 1, end_idx) essay = dict(ESSAYS[2]) essay["paragraphs"] = paragraphs return essay def extract_essay_4(tags: list[Tag]) -> dict: """Extract 'The Confidential Reasons for Lord Caitanya's Appearance' essay. Start:

containing "4th Chapter" Body: p6 paragraphs after start (skip editor note, skip p4/p5/p29/p30 headers) End: Next

after start (next chapter header "Ädi-lélä, Chapter 5") """ # Find the start marker: p30 containing "4th Chapter" start_idx = find_tag_index( tags, 0, lambda t: get_css_class(t) == "p30" and "4th Chapter" in t.get_text(), ) if start_idx is None: raise ValueError("Could not find Essay 4 start marker (p30 '4th Chapter')") # Find the end marker: next p4 after the start (next chapter header) end_idx = find_tag_index(tags, start_idx + 1, lambda t: get_css_class(t) == "p4") if end_idx is None: raise ValueError("Could not find Essay 4 end marker (p4)") print(f" Essay 4: tags[{start_idx}] → tags[{end_idx}] (exclusive)") # Collect p6 paragraphs, skipping editor notes paragraphs = extract_body_paragraphs_extended( tags, start_idx + 1, end_idx, body_classes={"p6"}, skip_editor_notes=True, ) essay = dict(ESSAYS[3]) essay["paragraphs"] = paragraphs return essay def extract_essay_5(tags: list[Tag]) -> dict: """Extract 'The Glories of Lord Nityānanda' essay. Start:

containing "1.V" (i.e. "Shi Chaitanya Charitamrita 1.V") Body: p6 and p34 paragraphs after start (skip editor note, skip p4/p5/p31 headers, skip p9 divider underscores) End: Next

after start ("Easy Journey To Other Planets") """ # Find the start marker: p31 containing "1.V" start_idx = find_tag_index( tags, 0, lambda t: get_css_class(t) == "p31" and "1.V" in t.get_text(), ) if start_idx is None: raise ValueError("Could not find Essay 5 start marker (p31 containing '1.V')") # Find the end marker: next p35 after the start end_idx = find_tag_index(tags, start_idx + 1, lambda t: get_css_class(t) == "p35") if end_idx is None: raise ValueError("Could not find Essay 5 end marker (p35)") print(f" Essay 5: tags[{start_idx}] → tags[{end_idx}] (exclusive)") # Collect p6 and p34 paragraphs, skipping editor notes and p9 dividers paragraphs = extract_body_paragraphs_extended( tags, start_idx + 1, end_idx, body_classes={"p6", "p34"}, skip_editor_notes=True, ) essay = dict(ESSAYS[4]) essay["paragraphs"] = paragraphs return essay # --------------------------------------------------------------------------- # Quality checks # --------------------------------------------------------------------------- def quality_check(essay: dict, essay_num: int) -> None: """Run quality checks on an extracted essay.""" paragraphs = essay["paragraphs"] title = essay["title"] print(f"\n--- Quality Checks: Essay {essay_num} ({title}) ---") # Check paragraph count is reasonable if len(paragraphs) < 5: print(f" ⚠ Only {len(paragraphs)} paragraphs (suspiciously low)") elif len(paragraphs) > 150: print(f" ⚠ {len(paragraphs)} paragraphs (suspiciously high)") else: print(f" ✅ {len(paragraphs)} paragraphs (reasonable)") # Check for residual span class artifacts span_count = sum(1 for p in paragraphs if 'class="' in p and "]+>", "", p).strip() if plain.startswith("[") and ("proofread" in plain.lower() or "—Ed" in plain): print(f" ⚠ Editor note leaked into paragraphs") break else: print(f" ✅ No editor notes leaked into paragraphs") # Check that divider lines were excluded for p in paragraphs: plain = re.sub(r"<[^>]+>", "", p).strip() if re.match(r"^_+$", plain): print(f" ⚠ Divider line leaked into paragraphs") break else: print(f" ✅ No divider lines leaked into paragraphs") # Essay-specific checks if essay_num == 1: if "spiritual master" in all_text.lower(): print(f" ✅ 'spiritual master' found in text") else: print(f" ⚠ 'spiritual master' not found") # Check that reference line [BTG 1960] was excluded for p in paragraphs: plain = re.sub(r"<[^>]+>", "", p).strip() if "BTG 1960" in plain and plain.startswith("["): print(f" ⚠ Reference line '[BTG 1960]' leaked into paragraphs") break else: print(f" ✅ Reference line properly excluded") # Check that title was excluded for p in paragraphs: plain = re.sub(r"<[^>]+>", "", p).strip() if ( plain == "(The Spiritual Masters)." or plain == "(The Spiritual Masters)." ): print(f" ⚠ Title line leaked into paragraphs") break else: print(f" ✅ Title line properly excluded") elif essay_num == 2: if "impersonal Brahman" in all_text or "Absolute Truth" in all_text: print(f" ✅ 'Absolute Truth' / 'impersonal Brahman' found") else: print(f" ⚠ Key terms for Essay 2 not found") # Check "(Second Part)" header was excluded for p in paragraphs: plain = re.sub(r"<[^>]+>", "", p).strip() if plain == "(Second Part)": print(f" ⚠ Header '(Second Part)' leaked into paragraphs") break else: print(f" ✅ Header '(Second Part)' properly excluded") elif essay_num == 3: if "incarnation" in all_text.lower() or "descent" in all_text.lower(): print(f" ✅ 'incarnation'/'descent' found") else: print(f" ⚠ Key terms for Essay 3 not found") # Check that "Thus ends the III part..." is included found_ending = False for p in paragraphs: if "Thus ends the III part" in p: found_ending = True break if found_ending: print(f" ✅ 'Thus ends the III part...' paragraph included") else: print(f" ⚠ 'Thus ends the III part...' paragraph missing") elif essay_num == 4: if "Radharani" in all_text or "confidential" in all_text.lower(): print(f" ✅ 'Radharani'/'confidential' found") else: print(f" ⚠ Key terms for Essay 4 not found") # Check that the summary paragraph is included (last paragraph) found_summary = False for p in paragraphs: if "author then summarize" in p.lower(): found_summary = True break if found_summary: print(f" ✅ Summary paragraph ('The author then summarize...') included") else: print(f" ⚠ Summary paragraph missing") # Check that the first body paragraph mentions "three principal purposes" if paragraphs: if "three principal purposes" in paragraphs[0]: print(f" ✅ First paragraph mentions 'three principal purposes'") else: print(f" ⚠ First paragraph doesn't mention 'three principal purposes'") # Check that p4/p5/p29/p30 header lines were excluded for p in paragraphs: plain = re.sub(r"<[^>]+>", "", p).strip() if "4th Chapter" in plain and len(plain) < 50: print(f" ⚠ Header '4th Chapter' leaked into paragraphs") break else: print(f" ✅ Header lines properly excluded") elif essay_num == 5: if "Nityananda" in all_text: print(f" ✅ 'Nityananda' found") else: print(f" ⚠ 'Nityananda' not found") # Check that the final p34 description paragraph is included if paragraphs: last_plain = re.sub(r"<[^>]+>", "", paragraphs[-1]).strip() if ( "lusture" in last_plain or "turban" in last_plain or "wonderful feature" in last_plain ): print(f" ✅ Final description paragraph (p34) included") else: print(f" ⚠ Final description paragraph (p34) may be missing") print(f" Last paragraph preview: {last_plain[:80]}...") # Check that the first body paragraph mentions "5th chapter" if paragraphs: if "5th chapter" in paragraphs[0].lower(): print(f" ✅ First paragraph mentions '5th chapter'") else: print(f" ⚠ First paragraph doesn't mention '5th chapter'") # Check that p31 header was excluded for p in paragraphs: plain = re.sub(r"<[^>]+>", "", p).strip() if "Shi Chaitanya Charitamrita 1.V" in plain: print( f" ⚠ Header 'Shi Chaitanya Charitamrita 1.V' leaked into paragraphs" ) break else: print(f" ✅ Header lines properly excluded") # Sample output print(f"\n First paragraph (preview):") if paragraphs: preview = re.sub(r"<[^>]+>", "", paragraphs[0])[:120] print(f" {preview}...") print(f" Last paragraph (preview):") if paragraphs: last = re.sub(r"<[^>]+>", "", paragraphs[-1])[:120] print(f" {last}...") # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- 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_dir = os.path.join(base_dir, "lib", "jsons", "essays") if not os.path.exists(html_path): print(f"ERROR: {html_path} not found!") sys.exit(1) print(f"Reading {html_path}...") tags = parse_all_paragraphs(html_path) print(f" Parsed {len(tags)}

tags total\n") # Extract all five essays print("Locating essays...") essay_1 = extract_essay_1(tags) essay_2 = extract_essay_2(tags) essay_3 = extract_essay_3(tags) essay_4 = extract_essay_4(tags) essay_5 = extract_essay_5(tags) all_essays = [essay_1, essay_2, essay_3, essay_4, essay_5] # Write JSON files print(f"\nWriting JSON files...") for essay in all_essays: output_path = os.path.join(output_dir, f"{essay['slug']}.json") write_json(essay, output_path) print(f" Written: {output_path}") # Quality checks for i, essay in enumerate(all_essays, 1): quality_check(essay, i) # Summary print(f"\n{'=' * 60}") print(f" EXTRACTION SUMMARY") print(f"{'=' * 60}") for i, essay in enumerate(all_essays, 1): print( f" Essay {i}: {essay['title']}" f" ({len(essay['paragraphs'])} paragraphs, {essay['year']})" ) print( f" slug={essay['slug']}, group={essay['group']}, order={essay['order']}" ) print(f"{'=' * 60}") print(f" Total essays: {len(all_essays)}") print(f" Total paragraphs: {sum(len(e['paragraphs']) for e in all_essays)}") print(f"\nDone! ✨") if __name__ == "__main__": main()