feat: add journals

This commit is contained in:
gosangam
2026-05-03 21:02:41 +05:30
parent 89106efc98
commit 35d1d14f37
47 changed files with 11365 additions and 37 deletions
+688
View File
@@ -0,0 +1,688 @@
#!/usr/bin/env python3
"""Extract The Jaladuta Diary (1965) from others-before-1977.html.
Each diary day entry becomes its own chapter file.
Chapters are grouped by month in the intro.json metadata.
Structure:
- Preface and Introduction are standalone chapters (slugs "preface", "introduction")
- Day entries use slugs like "apr-3", "may-2", "aug-13", "sep-1", etc.
- The "Memoranda" entry at the end uses slug "memoranda"
- Months are parent groups in intro.json (non-clickable headings)
Output:
lib/jsons/journals/jaladuta-diary/intro.json
lib/jsons/journals/jaladuta-diary/chapters/preface.json
lib/jsons/journals/jaladuta-diary/chapters/introduction.json
lib/jsons/journals/jaladuta-diary/chapters/apr-3.json
lib/jsons/journals/jaladuta-diary/chapters/apr-5.json
...
lib/jsons/journals/jaladuta-diary/chapters/nov-28.json
lib/jsons/journals/jaladuta-diary/chapters/memoranda.json
Usage:
python3 scripts/jaladuta_diary_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="s3">text</span> → <i>text</i>
html = re.sub(
r'<span[^>]*class="s3"[^>]*>(.*?)</span>', r"<i>\1</i>", html, flags=re.DOTALL
)
# <span class="s5">...</span> → keep inner text
html = re.sub(
r'<span[^>]*class="s5"[^>]*>(.*?)</span>', r"\1", html, flags=re.DOTALL
)
# <span class="s13">...</span> → keep inner text
html = re.sub(
r'<span[^>]*class="s1[0-9]+"[^>]*>(.*?)</span>', r"\1", html, flags=re.DOTALL
)
# <span class="s23">...</span> → keep inner text
html = re.sub(
r'<span[^>]*class="s2[0-9]+"[^>]*>(.*?)</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)
# Clean up Apple-tab-span leftovers
html = html.replace('<span class="Apple-tab-span">\t</span>', "\t")
# Collapse multiple spaces (but not around <br>)
html = re.sub(r"[ \t]+", " ", html)
html = html.strip()
# Remove leading <br> tags
html = re.sub(r"^(<br\s*/?>[\s]*)+", "", html)
return html
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")
# ---------------------------------------------------------------------------
# Month abbreviation helpers
# ---------------------------------------------------------------------------
MONTH_ABBR = {
"april": "apr",
"may": "may",
"june": "jun",
"july": "jul",
"august": "aug",
"september": "sep",
"october": "oct",
"november": "nov",
}
MONTH_ORDER = ["apr", "may", "jun", "jul", "aug", "sep", "oct", "nov"]
DAY_NAMES = {
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
}
def parse_day_header(bold_text: str):
"""Parse the bold header of a diary entry to extract day name and date number.
Handles formats like:
"SATURDAY 3"
"13 FRIDAY"
"SATURDAY" (date-less, like Aug 14)
"MEMORANDA"
Returns (day_name, date_number) or (None, None) if not a day entry.
"""
text = bold_text.strip().split("\n")[0].strip()
text = re.sub(r"<br\s*/?>", " ", text).strip()
if text.upper() == "MEMORANDA":
return ("MEMORANDA", None)
# Try "DAY NUMBER" pattern
m = re.match(r"([A-Za-z]+)\s+(\d+)", text)
if m:
day = m.group(1).upper()
num = int(m.group(2))
if day in {d.upper() for d in DAY_NAMES}:
return (day, num)
# Try "NUMBER DAY" pattern
m = re.match(r"(\d+)\s+([A-Za-z]+)", text)
if m:
num = int(m.group(1))
day = m.group(2).upper()
if day in {d.upper() for d in DAY_NAMES}:
return (day, num)
# Try just "DAY" (no number — like SATURDAY 14 where 14 is on next line)
word = text.split()[0].upper() if text.split() else ""
if word in {d.upper() for d in DAY_NAMES}:
# Try to find number after
rest = text[len(word) :].strip()
m2 = re.match(r"(\d+)", rest)
if m2:
return (word, int(m2.group(1)))
return (word, None)
return (None, 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", "others-before-1977.html")
output_dir = os.path.join(base_dir, "lib", "jsons", "journals", "jaladuta-diary")
chapters_dir = os.path.join(output_dir, "chapters")
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")
# Find the Jaladuta Diary section boundaries
start_idx = None
end_idx = None
for i, tag in enumerate(all_tags):
text = tag.get_text().strip()
cls = get_css_class(tag)
if cls == "p35" and "Jaladuta Diary" in text:
start_idx = i
if start_idx is not None and i > start_idx:
# End at "Prayers Written by Śrīla Prabhupāda aboard the Jaladuta" or the next p4 bookmark after JD
if cls == "p4" and "JD 8" in text:
end_idx = i
break
if start_idx is None:
print("ERROR: Could not find Jaladuta Diary start")
sys.exit(1)
if end_idx is None:
# Fallback: use the memoranda end
for i, tag in enumerate(all_tags):
if (
i > start_idx
and "MEMORANDA" in tag.get_text()
and get_css_class(tag) == "p110"
):
end_idx = i + 1
break
print(f" Section: tags[{start_idx}] → tags[{end_idx}]")
section_tags = all_tags[start_idx:end_idx]
# ---------------------------------------------------------------------------
# Phase 1: Extract Preface and Introduction
# ---------------------------------------------------------------------------
preface_paragraphs = []
intro_paragraphs = []
current_section = None # "preface" | "introduction" | None
# Also track where months/diary entries begin
diary_start_idx = None
for i, tag in enumerate(section_tags):
cls = get_css_class(tag)
text = tag.get_text().strip()
# Detect Preface header
if cls == "p3" and text == "Preface":
current_section = "preface"
continue
if cls == "p153": # bookmark
continue
# Detect Introduction header
if cls in ("p11", "p157") and "Introduction" in text:
if cls == "p157":
current_section = "introduction"
continue
elif cls == "p11":
current_section = "introduction"
continue
# Detect month start (diary entries begin)
if cls == "p11" and re.match(r"JD \d+:", text):
diary_start_idx = i
current_section = None
break
if cls == "p29":
# Bold month header like "APRIL 1965" — this is diary territory
diary_start_idx = i
current_section = None
break
# Collect preface/intro paragraphs
if current_section == "preface" and cls in ("p154", "p155", "p156"):
html = inner_html(tag)
if html:
preface_paragraphs.append(html)
elif current_section == "introduction" and cls in ("p154", "p155", "p156"):
html = inner_html(tag)
if html:
intro_paragraphs.append(html)
print(f"\n Preface: {len(preface_paragraphs)} paragraphs")
print(f" Introduction: {len(intro_paragraphs)} paragraphs")
# ---------------------------------------------------------------------------
# Phase 2: Extract diary entries grouped by month
# ---------------------------------------------------------------------------
# Walk through remaining tags, tracking current month
current_month = None # "apr", "may", etc.
current_month_full = None # "April 1965", etc.
# Section headers that appear mid-month
section_headers = {} # Maps tag index to header text
# All day entries: list of { month, day, date, slug, title, paragraphs }
day_entries = []
current_entry = None
MONTH_MAP = {
"APRIL": ("apr", "April 1965"),
"MAY": ("may", "May 1965"),
"JUNE": ("jun", "June 1965"),
"JULY": ("jul", "July 1965"),
"AUGUST": ("aug", "August 1965"),
"SEPTEMBER": ("sep", "September 1965"),
"OCTOBER": ("oct", "October 1965"),
"NOVEMBER": ("nov", "November 1965"),
}
def finalize_entry():
nonlocal current_entry
if current_entry and current_entry["paragraphs"]:
day_entries.append(current_entry)
current_entry = None
diary_tags = section_tags[diary_start_idx:] if diary_start_idx else []
for i, tag in enumerate(diary_tags):
cls = get_css_class(tag)
text = tag.get_text().strip()
# Skip bookmarks
if cls in ("p11", "p4", "p39", "p153"):
continue
# Detect month headers (p29 bold)
if cls == "p29":
html = inner_html(tag)
# Check if this is a month header like "APRIL 1965" or "MAY 1965"
for month_key, (abbr, full) in MONTH_MAP.items():
if month_key in text.upper():
finalize_entry()
current_month = abbr
current_month_full = full
break
else:
# Could be a section header like "Verse 1." — skip if not month
# Or it could be a section header within a month
pass
continue
# Detect section headers (p68 class — "The Journey", "America")
if cls == "p68":
finalize_entry()
html = inner_html(tag)
# Create a section-header entry
if html and current_month:
section_entry = {
"month": current_month,
"month_full": current_month_full,
"day": None,
"date": None,
"slug": None,
"title": None,
"paragraphs": [f"<b>{re.sub('<[^>]+>', ' ', html).strip()}</b>"],
"is_section_header": True,
}
# Extract title text
plain = re.sub("<[^>]+>", " ", html)
plain = re.sub(r"\s+", " ", plain).strip()
section_entry["title"] = plain
day_entries.append(section_entry)
continue
# Detect diary entries (p110 class)
if cls == "p110":
finalize_entry()
html = inner_html(tag)
if not html:
continue
# Parse the bold header
b_tag = tag.find("b")
if b_tag:
bold_text = b_tag.get_text().strip()
day_name, date_num = parse_day_header(bold_text)
else:
day_name, date_num = (None, None)
if day_name == "MEMORANDA":
current_entry = {
"month": current_month,
"month_full": current_month_full,
"day": "MEMORANDA",
"date": None,
"slug": "memoranda",
"title": "Memoranda",
"paragraphs": [html],
"is_section_header": False,
}
elif day_name and date_num and current_month:
slug = f"{current_month}-{date_num}"
title = f"{day_name.capitalize()} {date_num}"
current_entry = {
"month": current_month,
"month_full": current_month_full,
"day": day_name,
"date": date_num,
"slug": slug,
"title": title,
"paragraphs": [html],
"is_section_header": False,
}
elif day_name and current_month:
# Day without date number (rare — like "SATURDAY" alone on Aug 14)
# Try to infer from context or just skip
slug = f"{current_month}-{day_name.lower()}"
title = day_name.capitalize()
current_entry = {
"month": current_month,
"month_full": current_month_full,
"day": day_name,
"date": None,
"slug": slug,
"title": title,
"paragraphs": [html],
"is_section_header": False,
}
else:
# Can't parse — treat as continuation or standalone
current_entry = {
"month": current_month,
"month_full": current_month_full,
"day": None,
"date": None,
"slug": None,
"title": text[:50],
"paragraphs": [html],
"is_section_header": False,
}
continue
# Detect p12 paragraphs (continuation text in some entries like Sep 16)
if cls == "p12" and current_entry:
html = inner_html(tag)
if html:
current_entry["paragraphs"].append(html)
continue
# p155 (source citations within diary section)
if cls == "p155" and current_entry:
html = inner_html(tag)
if html:
current_entry["paragraphs"].append(html)
continue
finalize_entry()
print(f"\n Total day entries extracted: {len(day_entries)}")
# Count by month
month_counts = {}
for entry in day_entries:
m = entry["month"]
month_counts[m] = month_counts.get(m, 0) + 1
for m in MONTH_ORDER:
if m in month_counts:
print(f" {m}: {month_counts[m]} entries")
# ---------------------------------------------------------------------------
# Phase 3: Fix up entries — handle dateless entries, dedup slugs
# ---------------------------------------------------------------------------
# Some entries have no date — try to fix known cases
# Aug "SATURDAY" without number is Aug 14 (follows Aug 13 FRIDAY)
for i, entry in enumerate(day_entries):
if entry.get("is_section_header"):
continue
if entry["date"] is None and entry["day"] and entry["day"] != "MEMORANDA":
# Try to infer from previous entry
if i > 0:
prev = day_entries[i - 1]
if prev["date"] and prev["month"] == entry["month"]:
inferred = prev["date"] + 1
entry["date"] = inferred
entry["slug"] = f"{entry['month']}-{inferred}"
entry["title"] = f"{entry['day'].capitalize()} {inferred}"
print(
f" Inferred date: {entry['slug']} (from previous {prev['slug']})"
)
# Ensure unique slugs
seen_slugs = set()
for entry in day_entries:
if entry.get("is_section_header"):
continue
slug = entry["slug"]
if slug and slug in seen_slugs:
# Append suffix
suffix = 2
while f"{slug}-{suffix}" in seen_slugs:
suffix += 1
entry["slug"] = f"{slug}-{suffix}"
print(f" Deduped slug: {slug} → {entry['slug']}")
if slug:
seen_slugs.add(entry["slug"])
# Merge section headers into the next entry's paragraphs (prepend)
merged_entries = []
pending_header = None
for entry in day_entries:
if entry.get("is_section_header"):
pending_header = entry["paragraphs"][0]
continue
if pending_header:
entry["paragraphs"] = [pending_header] + entry["paragraphs"]
pending_header = None
merged_entries.append(entry)
day_entries = merged_entries
# ---------------------------------------------------------------------------
# Phase 4: Build intro.json with hierarchical month groups
# ---------------------------------------------------------------------------
# Assign chapter numbers: preface=1, introduction=2, then day entries sequentially
chapter_num = 1
all_chapters = []
# Preface
preface_chapter = {
"slug": "preface",
"title": "Preface",
"number": chapter_num,
}
all_chapters.append(preface_chapter)
chapter_num += 1
# Introduction
intro_chapter = {
"slug": "introduction",
"title": "Introduction",
"number": chapter_num,
}
all_chapters.append(intro_chapter)
chapter_num += 1
# Group day entries by month
months_data = [] # List of { month, title, subChapters: [...] }
current_month_group = None
for entry in day_entries:
month = entry["month"]
month_full = entry["month_full"]
if current_month_group is None or current_month_group["month"] != month:
current_month_group = {
"month": month,
"title": month_full,
"subChapters": [],
}
months_data.append(current_month_group)
entry["number"] = chapter_num
current_month_group["subChapters"].append(
{
"slug": entry["slug"],
"title": entry["title"],
}
)
chapter_num += 1
# Build the intro.json chapters array
# Front matter chapters (preface, introduction) are top-level
# Month groups use the hierarchical sub-chapter pattern
intro_chapters = []
# Add preface and introduction as simple chapters
intro_chapters.append({"slug": "preface", "title": "Preface"})
intro_chapters.append({"slug": "introduction", "title": "Introduction"})
# Add month groups
for mg in months_data:
intro_chapters.append(
{
"title": mg["title"],
"subChapters": mg["subChapters"],
}
)
intro_json = {
"book": "jaladuta-diary",
"title": "The Jaladuta Diary, 1965 — Journey to the U.S.A.",
"author": "His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda",
"year": 1965,
"description": "Śrīla Prabhupāda's personal diary kept during his historic voyage from Calcutta to New York aboard the cargo ship Jaladuta in 1965.",
"totalChapters": chapter_num - 1,
"chapters": intro_chapters,
}
# ---------------------------------------------------------------------------
# Phase 5: Write JSON files
# ---------------------------------------------------------------------------
# Clean output directory
if os.path.exists(chapters_dir):
for f in os.listdir(chapters_dir):
os.remove(os.path.join(chapters_dir, f))
# Write intro.json
write_json(intro_json, os.path.join(output_dir, "intro.json"))
print(f"\n Written: intro.json ({len(intro_chapters)} top-level entries)")
# Write preface
write_json(
{
"book": "jaladuta-diary",
"chapter": 1,
"title": "Preface",
"type": "preface",
"slug": "preface",
"paragraphs": preface_paragraphs,
},
os.path.join(chapters_dir, "preface.json"),
)
print(f" Written: preface.json ({len(preface_paragraphs)} paragraphs)")
# Write introduction
write_json(
{
"book": "jaladuta-diary",
"chapter": 2,
"title": "Introduction",
"type": "introduction",
"slug": "introduction",
"paragraphs": intro_paragraphs,
},
os.path.join(chapters_dir, "introduction.json"),
)
print(f" Written: introduction.json ({len(intro_paragraphs)} paragraphs)")
# Write day entries
ch_num = 3
for entry in day_entries:
slug = entry["slug"]
if not slug:
print(f" ⚠ Skipping entry with no slug: {entry.get('title', '?')}")
continue
chapter_data = {
"book": "jaladuta-diary",
"chapter": ch_num,
"title": entry["title"],
"type": "chapter",
"slug": slug,
"paragraphs": entry["paragraphs"],
}
write_json(chapter_data, os.path.join(chapters_dir, f"{slug}.json"))
ch_num += 1
total_files = 2 + len([e for e in day_entries if e.get("slug")])
print(f"\n Total chapter files written: {total_files}")
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
print(f"\n{'=' * 60}")
print(f" EXTRACTION SUMMARY")
print(f"{'=' * 60}")
print(f" Preface: {len(preface_paragraphs)} paragraphs")
print(f" Introduction: {len(intro_paragraphs)} paragraphs")
print(f" Day entries: {len(day_entries)}")
for mg in months_data:
print(f" {mg['title']}: {len(mg['subChapters'])} entries")
for sc in mg["subChapters"]:
print(f" - {sc['slug']}: {sc['title']}")
print(f" Total chapters: {ch_num - 1}")
print(f"{'=' * 60}")
print(f"\nDone! ✨")
if __name__ == "__main__":
main()