feat: letters added

This commit is contained in:
gosangam
2026-05-04 00:08:31 +05:30
parent 35d1d14f37
commit 401766928a
20 changed files with 4272 additions and 26 deletions
+546
View File
@@ -0,0 +1,546 @@
#!/usr/bin/env python3
"""Extract Back to Godhead magazine from btg-1960.html.
Each magazine issue becomes its own JSON file.
Issues are grouped by volume in the intro.json metadata.
Structure:
- p2 = Volume/Part bookmark (e.g., "BTGPY1: Volume 1, Part 1, 1944")
- p3 = Volume/Part display title
- p8 = Article bookmark within an issue (e.g., "BTGPY1a: Message of His Divine Grace")
- p9 = Article display title (bold)
- p12, p35 = Body paragraphs
- p14, p17, p88 = Continuation notes / misc text
- p15 = Verse blocks (italic, with ScaGoudy encoding)
- p33, p40, p47, p54 = Magazine mastheads/headers
Output:
lib/jsons/magazines/btg/intro.json
lib/jsons/magazines/btg/issues/v1-p1.json
lib/jsons/magazines/btg/issues/v1-p2.json
...
lib/jsons/magazines/btg/issues/v4-p1.json
Usage:
python3 scripts/btg_extract.py
"""
import json
import os
import re
import sys
from bs4 import BeautifulSoup, Tag
# ---------------------------------------------------------------------------
# ScaGoudy → IAST Unicode mapping
# ---------------------------------------------------------------------------
SCAGOUDY_TO_IAST = {
"ä": "ā",
"Ä": "Ā",
"å": "ṛ",
"Å": "Ṛ",
"ñ": "ṣ",
"Ñ": "Ṣ",
"ö": "ṭ",
"Ö": "Ṭ",
"ë": "ṇ",
"Ë": "Ṇ",
"ò": "ḍ",
"Ò": "Ḍ",
"ç": "ś",
"Ç": "Ś",
"ì": "ṅ",
"Ì": "Ṅ",
"ï": "ñ",
"Ï": "Ñ",
"ü": "ū",
"Ü": "Ū",
"é": "ī",
"É": "Ī",
"à": "ṁ",
"ù": "ḥ",
"è": "ṝ",
"ÿ": "ḷ",
}
def decode_scagoudy(text: str) -> str:
"""Replace ScaGoudy-encoded characters with proper IAST Unicode."""
for src, dst in SCAGOUDY_TO_IAST.items():
text = text.replace(src, dst)
return text
def get_css_class(tag: Tag) -> str:
classes = tag.get("class", [])
return classes[0] if classes else ""
def inner_html(tag: Tag) -> str:
"""Get inner HTML of a tag, decode ScaGoudy, clean up spans."""
html = "".join(str(child) for child in tag.children)
html = decode_scagoudy(html)
# <span class="s1">text</span> → keep inner text (s1 is a ref style)
html = re.sub(
r'<span[^>]*class="s1"[^>]*>(.*?)</span>', r"\1", html, flags=re.DOTALL
)
# <span class="s3">text</span> → keep inner text (superscript digit)
html = re.sub(
r'<span[^>]*class="s3"[^>]*>(.*?)</span>', r"\1", html, flags=re.DOTALL
)
# <span class="s4">text</span> → keep inner text
html = re.sub(
r'<span[^>]*class="s4"[^>]*>(.*?)</span>', r"\1", html, flags=re.DOTALL
)
# <span class="s5">text</span> → keep inner text
html = re.sub(
r'<span[^>]*class="s5"[^>]*>(.*?)</span>', r"\1", html, flags=re.DOTALL
)
# <span class="s7">text</span> → keep inner text
html = re.sub(
r'<span[^>]*class="s7"[^>]*>(.*?)</span>', r"\1", html, flags=re.DOTALL
)
# <span class="s8">text</span> → keep inner text
html = re.sub(
r'<span[^>]*class="s8"[^>]*>(.*?)</span>', r"\1", html, flags=re.DOTALL
)
# Remove any remaining spans but keep content
html = re.sub(r"<span[^>]*>(.*?)</span>", r"\1", html, flags=re.DOTALL)
# Remove Apple-tab-span
html = html.replace("\t", " ")
# Collapse multiple spaces (but not around <br>)
html = re.sub(r"[ ]+", " ", html)
html = html.strip()
# Remove leading <br> tags
html = re.sub(r"^(<br\s*/?>[\s]*)+", "", html)
# Remove trailing <br> tags
html = re.sub(r"(<br\s*/?>[\s]*)+$", "", html)
return html
def is_verse_paragraph(tag: Tag) -> bool:
"""Check if a paragraph is a verse block (italic with line breaks)."""
cls = get_css_class(tag)
# p15 is commonly used for verse blocks
if cls == "p15":
return True
# Also check for paragraphs that are entirely italic with <br> breaks
# (some verses use p14 or other classes but are entirely in <i> tags)
return False
def format_verse(tag: Tag) -> str:
"""Format a verse paragraph as a center-aligned div."""
html = inner_html(tag)
if not html:
return ""
# Wrap in center-aligned div
return f'<div style="text-align:center">{html}</div>'
def write_json(data: dict, path: str):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
# ---------------------------------------------------------------------------
# Parse volume/part info from p2 bookmark text
# ---------------------------------------------------------------------------
def parse_volume_part(text: str):
"""Parse 'BTGPY1: Volume 1, Part 1, 1944' → (volume, part, date_str, slug).
Returns (volume_num, part_num, date_string, slug) or None.
"""
# Remove the BTGPY prefix
text = text.strip()
# Match "Volume X, Part Y, ..."
m = re.search(r"Volume\s+(\d+),?\s*Part\s+(\d+),?\s*(.*)", text, re.IGNORECASE)
if m:
vol = int(m.group(1))
part = int(m.group(2))
date_str = m.group(3).strip().rstrip(" ")
slug = f"v{vol}-p{part}"
return vol, part, date_str, slug
return None
# ---------------------------------------------------------------------------
# Main extraction
# ---------------------------------------------------------------------------
def main():
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
html_path = os.path.join(base_dir, "public", "html", "btg-1960.html")
output_dir = os.path.join(base_dir, "lib", "jsons", "magazines", "btg")
issues_dir = os.path.join(output_dir, "issues")
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:
content = f.read()
soup = BeautifulSoup(content, "html.parser")
all_tags = soup.find_all("p")
print(f" Parsed {len(all_tags)} <p> tags total")
# ---------------------------------------------------------------------------
# Phase 1: Find all issue boundaries
# ---------------------------------------------------------------------------
# Each issue starts with a p2 bookmark tag
issue_boundaries = [] # List of (tag_index, volume, part, date_str, slug)
for i, tag in enumerate(all_tags):
cls = get_css_class(tag)
if cls == "p2":
text = tag.get_text().strip()
parsed = parse_volume_part(text)
if parsed:
vol, part, date_str, slug = parsed
issue_boundaries.append((i, vol, part, date_str, slug))
print(f"\n Found {len(issue_boundaries)} issues")
for ib in issue_boundaries:
print(f" tag[{ib[0]}]: Vol {ib[1]}, Part {ib[2]}, {ib[3]} → {ib[4]}")
# ---------------------------------------------------------------------------
# Phase 2: Extract articles and paragraphs for each issue
# ---------------------------------------------------------------------------
# Classes to skip entirely
SKIP_CLASSES = {"p2", "p11", "p24", "p28"}
# Classes that are header/masthead material (include as paragraphs)
MASTHEAD_CLASSES = {"p3", "p4", "p5", "p6", "p7", "p32", "p33", "p34", "p40", "p47"}
# Classes that are body text
BODY_CLASSES = {
"p10",
"p12",
"p13",
"p14",
"p15",
"p16",
"p17",
"p35",
"p36",
"p37",
"p38",
"p39",
"p41",
"p42",
"p43",
"p44",
"p45",
"p46",
"p48",
"p49",
"p50",
"p51",
"p52",
"p53",
"p54",
"p55",
"p56",
"p57",
"p58",
"p59",
"p60",
"p61",
"p62",
"p63",
"p64",
"p65",
"p66",
"p67",
"p68",
"p69",
"p70",
"p71",
"p72",
"p73",
"p74",
"p75",
"p76",
"p77",
"p78",
"p79",
"p80",
"p81",
"p82",
"p83",
"p84",
"p85",
"p86",
"p87",
"p88",
"p89",
"p90",
"p91",
"p92",
"p93",
"p94",
"p95",
"p96",
"p97",
"p98",
"p99",
}
issues_data = []
for idx, (start_tag_idx, vol, part, date_str, slug) in enumerate(issue_boundaries):
# Determine end boundary
if idx + 1 < len(issue_boundaries):
end_tag_idx = issue_boundaries[idx + 1][0]
else:
end_tag_idx = len(all_tags)
issue_tags = all_tags[start_tag_idx:end_tag_idx]
paragraphs = []
articles = [] # List of { title, slug }
for tag in issue_tags:
cls = get_css_class(tag)
text = tag.get_text().strip()
# Skip bookmark tags and empty lines
if cls in SKIP_CLASSES:
continue
if not text and cls not in ("p15",):
continue
# p8 = Article bookmark — extract title for metadata
if cls == "p8":
# Extract article title from bookmark text (remove BTGPYxx: prefix)
article_text = text
m = re.match(r"BTGPY\d+[a-z]?:\s*(.*)", article_text)
if m:
article_title = m.group(1).strip()
else:
article_title = article_text
article_title = decode_scagoudy(article_title)
articles.append({"title": article_title})
continue
# p9 = Article display title — render as bold heading
if cls == "p9":
html = inner_html(tag)
if html:
# Make it a bold article heading
if not html.startswith("<b>"):
html = f"<b>{html}</b>"
paragraphs.append(html)
continue
# p15 = Verse block — center align
if cls == "p15":
html = format_verse(tag)
if html:
paragraphs.append(html)
continue
# p3 = Volume title — skip (it's the display title of the issue header)
if cls == "p3":
continue
# Masthead classes — include but as regular paragraphs
if cls in MASTHEAD_CLASSES:
html = inner_html(tag)
if html and html != "<br>":
paragraphs.append(html)
continue
# Body text classes
html = inner_html(tag)
if html and html != "<br>":
# Check if this looks like a verse (entirely italic with <br> breaks)
stripped = html.strip()
if (
stripped.startswith("<i>")
and "<br" in stripped
and not '<div style="text-align:center">' in stripped
):
# Check if it's predominantly a verse (italic block with line breaks)
# by seeing if removing the <i> tags leaves mostly text with <br>
plain = re.sub(r"</?[bi]>", "", stripped)
lines = [
l.strip() for l in re.split(r"<br\s*/?>", plain) if l.strip()
]
if len(lines) >= 2:
html = f'<div style="text-align:center">{stripped}</div>'
paragraphs.append(html)
# Build display title
if date_str:
display_title = f"Volume {vol}, Part {part}, {date_str}"
else:
display_title = f"Volume {vol}, Part {part}"
issue_data = {
"book": "btg",
"volume": vol,
"part": part,
"title": display_title,
"slug": slug,
"articles": articles,
"paragraphs": paragraphs,
}
issues_data.append(issue_data)
print(
f" {slug}: {display_title} — "
f"{len(articles)} articles, {len(paragraphs)} paragraphs"
)
# ---------------------------------------------------------------------------
# Phase 3: Build intro.json with hierarchical volume groups
# ---------------------------------------------------------------------------
# Group issues by volume
volume_groups = {}
for issue in issues_data:
vol = issue["volume"]
if vol not in volume_groups:
volume_groups[vol] = []
volume_groups[vol].append(issue)
# Build chapters array for intro.json
chapters = []
for vol_num in sorted(volume_groups.keys()):
vol_issues = volume_groups[vol_num]
sub_chapters = []
for issue in vol_issues:
sub_chapters.append(
{
"slug": issue["slug"],
"title": issue["title"],
}
)
chapters.append(
{
"title": f"Volume {vol_num}",
"subChapters": sub_chapters,
}
)
total_issues = len(issues_data)
total_articles = sum(len(i["articles"]) for i in issues_data)
intro_json = {
"book": "btg",
"title": "Back to Godhead",
"author": "His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda",
"year": 1944,
"description": f"The original Back to Godhead magazine founded in 1944. {total_issues} issues across {len(volume_groups)} volumes, containing {total_articles} articles on Kṛṣṇa consciousness, Vedic philosophy, and spiritual life.",
"totalIssues": total_issues,
"totalArticles": total_articles,
"chapters": chapters,
}
# ---------------------------------------------------------------------------
# Phase 4: Write JSON files
# ---------------------------------------------------------------------------
# Clean output directory
if os.path.exists(issues_dir):
for f in os.listdir(issues_dir):
os.remove(os.path.join(issues_dir, f))
# Write intro.json
write_json(intro_json, os.path.join(output_dir, "intro.json"))
print(f"\n Written: intro.json ({total_issues} issues, {total_articles} articles)")
# Write issue files
order = 100
for issue in issues_data:
issue_json = {
"book": "btg",
"volume": issue["volume"],
"part": issue["part"],
"title": issue["title"],
"slug": issue["slug"],
"order": order,
"articles": issue["articles"],
"paragraphs": issue["paragraphs"],
}
filepath = os.path.join(issues_dir, f"{issue['slug']}.json")
write_json(issue_json, filepath)
order += 1
print(f" Written: {len(issues_data)} issue files")
# ---------------------------------------------------------------------------
# Phase 5: Quality checks
# ---------------------------------------------------------------------------
print(f"\n{'=' * 60}")
print(f" QUALITY CHECKS")
print(f"{'=' * 60}")
# Check for ScaGoudy remnants
sca_chars = set(SCAGOUDY_TO_IAST.keys())
sca_remnants = 0
for issue in issues_data:
for p in issue["paragraphs"]:
if any(c in p for c in sca_chars if c not in ("é", "ü", "à")):
# Skip common French/German chars that might be legitimate
# Focus on clearly ScaGoudy-only chars
if "å" in p or "ë" in p or "ö" in p and "Ö" not in p:
sca_remnants += 1
break
if sca_remnants == 0:
print(f" ✅ No ScaGoudy remnants detected")
else:
print(f" ⚠ {sca_remnants} issues may have ScaGoudy remnants")
# Check for empty issues
empty_issues = [i for i in issues_data if len(i["paragraphs"]) == 0]
if empty_issues:
print(
f" ⚠ {len(empty_issues)} empty issues: {[i['slug'] for i in empty_issues]}"
)
else:
print(f" ✅ No empty issues")
# Check centered verses
verse_count = 0
for issue in issues_data:
for p in issue["paragraphs"]:
if '<div style="text-align:center">' in p:
verse_count += 1
print(f" ℹ️ {verse_count} center-aligned verse paragraphs")
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
print(f"\n{'=' * 60}")
print(f" EXTRACTION SUMMARY")
print(f"{'=' * 60}")
for vol_num in sorted(volume_groups.keys()):
vol_issues = volume_groups[vol_num]
vol_articles = sum(len(i["articles"]) for i in vol_issues)
vol_paras = sum(len(i["paragraphs"]) for i in vol_issues)
print(
f" Volume {vol_num}: {len(vol_issues)} issues, {vol_articles} articles, {vol_paras} paragraphs"
)
for issue in vol_issues:
print(
f" {issue['slug']}: {issue['title']} "
f"({len(issue['articles'])} articles, {len(issue['paragraphs'])} paras)"
)
print(f"\n Total: {total_issues} issues, {total_articles} articles")
print(f"{'=' * 60}")
print(f"\nDone! ✨")
if __name__ == "__main__":
main()