diff --git a/app/books/bg/[chapter]/[verse]/page.tsx b/app/books/bg/[chapter]/[verse]/page.tsx new file mode 100644 index 00000000..47dad7ef --- /dev/null +++ b/app/books/bg/[chapter]/[verse]/page.tsx @@ -0,0 +1,77 @@ +import Link from "next/link"; +import { PageHeader } from "@/components/page-header"; +import { notFound } from "next/navigation"; +import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api"; +import { ChapterViewer } from "@/app/books/chapter-viewer"; + +type BgMeta = { + book: string; + title: string; + author: string; + totalChapters: number; + chapters: { number: number; title: string; totalVerses: number }[]; +}; + +export default async function VersePage({ + params, +}: { + params: Promise<{ chapter: string; verse: string }>; +}) { + const { chapter, verse: verseParam } = await params; + + const meta = await getBookMeta("bg"); + if (!meta) notFound(); + + const bgMeta = meta as unknown as BgMeta; + const chapterInfo = bgMeta.chapters?.find( + (c) => c.number === parseInt(chapter), + ); + if (!chapterInfo?.title) notFound(); + + const title: string = chapterInfo.title; + + const verseIndex = await getChapterVerseIndex("bg", null, parseInt(chapter)); + if (verseIndex.length === 0) notFound(); + + const requestedVerse = parseInt(verseParam); + const verse = await getSingleVerse( + "bg", + null, + parseInt(chapter), + requestedVerse, + ); + if (!verse) notFound(); + + return ( +
+ + + ← Bhagavad-gītā + +

+ Chapter {chapter}: {title} +

+
+ +
+ ({ + verse: v.verse, + verseEnd: v.verseEnd ?? undefined, + reference: v.reference, + }))} + book="bg" + division={null} + chapter={parseInt(chapter)} + basePath={`/books/bg/${chapter}`} + showBengali={false} + /> +
+
+ ); +} diff --git a/app/books/bg/[chapter]/page.tsx b/app/books/bg/[chapter]/page.tsx index 1c85ee70..ef20651a 100644 --- a/app/books/bg/[chapter]/page.tsx +++ b/app/books/bg/[chapter]/page.tsx @@ -1,81 +1,16 @@ -import Link from "next/link"; -import { PageHeader } from "@/components/page-header"; +import { redirect } from "next/navigation"; import { notFound } from "next/navigation"; -import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api"; -import { ChapterViewer } from "@/app/books/chapter-viewer"; - -type BgMeta = { - book: string; - title: string; - author: string; - totalChapters: number; - chapters: { number: number; title: string; totalVerses: number }[]; -}; +import { getChapterVerseIndex } from "@/lib/api"; export default async function ChapterPage({ params, - searchParams, }: { params: Promise<{ chapter: string }>; - searchParams: Promise<{ verse?: string }>; }) { const { chapter } = await params; - const { verse: verseParam } = await searchParams; - - const meta = await getBookMeta("bg"); - if (!meta) notFound(); - - const bgMeta = meta as unknown as BgMeta; - const chapterInfo = bgMeta.chapters?.find( - (c) => c.number === parseInt(chapter), - ); - if (!chapterInfo?.title) notFound(); - - const title: string = chapterInfo.title; - const verseIndex = await getChapterVerseIndex("bg", null, parseInt(chapter)); - if (verseIndex.length === 0) notFound(); - - const requestedVerse = verseParam - ? parseInt(verseParam) - : verseIndex[0].verse; - const verse = await getSingleVerse( - "bg", - null, - parseInt(chapter), - requestedVerse, - ); - if (!verse) notFound(); - - return ( -
- - - ← Bhagavad-gītā - -

- Chapter {chapter}: {title} -

-
- -
- ({ - verse: v.verse, - verseEnd: v.verseEnd ?? undefined, - reference: v.reference, - }))} - book="bg" - division={null} - chapter={parseInt(chapter)} - showBengali={false} - /> -
-
- ); + if (verseIndex.length === 0) { + notFound(); + } + redirect(`/books/bg/${chapter}/${verseIndex[0].verse}`); } diff --git a/app/books/cc/[lila]/[chapter]/[verse]/page.tsx b/app/books/cc/[lila]/[chapter]/[verse]/page.tsx new file mode 100644 index 00000000..c9ac0062 --- /dev/null +++ b/app/books/cc/[lila]/[chapter]/[verse]/page.tsx @@ -0,0 +1,84 @@ +import Link from "next/link"; +import { PageHeader } from "@/components/page-header"; +import { notFound } from "next/navigation"; +import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api"; +import { ChapterViewer } from "@/app/books/chapter-viewer"; + +type ChapterInfo = { + number: number; + title: string; + totalVerses: number; +}; + +type LilaInfo = { + key: string; + name: string; + totalChapters: number; + totalVerses: number; + chapters: ChapterInfo[]; +}; + +export default async function CcVersePage({ + params, +}: { + params: Promise<{ lila: string; chapter: string; verse: string }>; +}) { + const { lila, chapter, verse } = await params; + + const meta = await getBookMeta("cc"); + if (!meta) notFound(); + + const lilas = meta.lilas as LilaInfo[] | undefined; + const lilaInfo = lilas?.find((l) => l.key === lila); + if (!lilaInfo) notFound(); + + const chapterInfo = lilaInfo.chapters.find( + (ch) => ch.number === parseInt(chapter), + ); + if (!chapterInfo) notFound(); + + const verseIndex = await getChapterVerseIndex("cc", lila, parseInt(chapter)); + if (verseIndex.length === 0) notFound(); + + const requestedVerse = parseInt(verse); + const verseData = await getSingleVerse( + "cc", + lila, + parseInt(chapter), + requestedVerse, + ); + if (!verseData) notFound(); + + return ( +
+ + + ← {lilaInfo.name} + +

+ Chapter {chapter}: {chapterInfo.title} +

+
+ +
+ ({ + verse: v.verse, + verseEnd: v.verseEnd ?? undefined, + reference: v.reference, + }))} + book="cc" + division={lila} + chapter={parseInt(chapter)} + showBengali={true} + basePath={`/books/cc/${lila}/${chapter}`} + /> +
+
+ ); +} diff --git a/app/books/cc/[lila]/[chapter]/page.tsx b/app/books/cc/[lila]/[chapter]/page.tsx index cbf885bf..8b0a966d 100644 --- a/app/books/cc/[lila]/[chapter]/page.tsx +++ b/app/books/cc/[lila]/[chapter]/page.tsx @@ -1,88 +1,16 @@ -import Link from "next/link"; -import { PageHeader } from "@/components/page-header"; +import { redirect } from "next/navigation"; import { notFound } from "next/navigation"; -import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api"; -import { ChapterViewer } from "@/app/books/chapter-viewer"; - -type ChapterInfo = { - number: number; - title: string; - totalVerses: number; -}; - -type LilaInfo = { - key: string; - name: string; - totalChapters: number; - totalVerses: number; - chapters: ChapterInfo[]; -}; +import { getChapterVerseIndex } from "@/lib/api"; export default async function CcChapterPage({ params, - searchParams, }: { params: Promise<{ lila: string; chapter: string }>; - searchParams: Promise<{ verse?: string }>; }) { const { lila, chapter } = await params; - const { verse: verseParam } = await searchParams; - - const meta = await getBookMeta("cc"); - if (!meta) notFound(); - - const lilas = meta.lilas as LilaInfo[] | undefined; - const lilaInfo = lilas?.find((l) => l.key === lila); - if (!lilaInfo) notFound(); - - const chapterInfo = lilaInfo.chapters.find( - (ch) => ch.number === parseInt(chapter), - ); - if (!chapterInfo) notFound(); - const verseIndex = await getChapterVerseIndex("cc", lila, parseInt(chapter)); - if (verseIndex.length === 0) notFound(); - - const requestedVerse = verseParam - ? parseInt(verseParam) - : verseIndex[0].verse; - const verse = await getSingleVerse( - "cc", - lila, - parseInt(chapter), - requestedVerse, - ); - if (!verse) notFound(); - - return ( -
- - - ← {lilaInfo.name} - -

- Chapter {chapter}: {chapterInfo.title} -

-
- -
- ({ - verse: v.verse, - verseEnd: v.verseEnd ?? undefined, - reference: v.reference, - }))} - book="cc" - division={lila} - chapter={parseInt(chapter)} - showBengali={true} - /> -
-
- ); + if (verseIndex.length === 0) { + notFound(); + } + redirect(`/books/cc/${lila}/${chapter}/${verseIndex[0].verse}`); } diff --git a/app/books/chapter-viewer.tsx b/app/books/chapter-viewer.tsx index 939e3164..ecc68f41 100644 --- a/app/books/chapter-viewer.tsx +++ b/app/books/chapter-viewer.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useCallback, useMemo, useEffect, useState } from "react"; import { Card, CardHeader, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; @@ -58,6 +58,8 @@ type ChapterViewerProps = { chapter: number; /** Whether to show the Bengali script field (CC only) */ showBengali?: boolean; + /** Base path for verse URLs, e.g. "/books/bg/1" — verse number is appended */ + basePath: string; }; // --------------------------------------------------------------------------- @@ -217,9 +219,9 @@ export function ChapterViewer({ division, chapter, showBengali = false, + basePath, }: ChapterViewerProps) { const router = useRouter(); - const searchParams = useSearchParams(); // Verse cache: verseNum → Verse data const [cache, setCache] = useState>(() => { @@ -230,17 +232,9 @@ export function ChapterViewer({ const [loading, setLoading] = useState(false); - // Current verse number from URL (or default to initial) - const currentVerseNum = useMemo(() => { - const param = searchParams.get("verse"); - if (param) { - const num = parseInt(param); - if (!isNaN(num) && verseIndex.some((v) => v.verse === num)) { - return num; - } - } - return initialVerseNum; - }, [searchParams, initialVerseNum, verseIndex]); + // Current verse number — driven by the initialVerseNum from the server + // (which reads it from the [verse] path param) + const currentVerseNum = initialVerseNum; // Current index in the verseIndex array const currentIdx = useMemo( @@ -304,23 +298,21 @@ export function ChapterViewer({ [cache, book, division, chapter], ); - // Navigate to a verse — update the URL and fetch if needed + // Navigate to a verse — update the URL path and fetch if needed const navigateTo = useCallback( async (verseNum: number) => { - // Update URL with shallow navigation - const params = new URLSearchParams(searchParams.toString()); - params.set("verse", String(verseNum)); - router.push(`?${params.toString()}`, { scroll: false }); - - // Fetch if not cached + // Prefetch the verse data before navigating if (!cache.has(verseNum)) { await fetchVerse(verseNum); } + // Navigate to the new path-based URL + router.push(`${basePath}/${verseNum}`, { scroll: false }); + // Scroll to top of the verse window.scrollTo({ top: 0, behavior: "smooth" }); }, - [searchParams, router, cache, fetchVerse], + [basePath, router, cache, fetchVerse], ); // Fetch current verse on mount/URL change if not cached @@ -390,7 +382,7 @@ export function ChapterViewer({ return ( { e.preventDefault(); navigateTo(v.verse); diff --git a/app/books/page.tsx b/app/books/page.tsx index bbbdb75f..c8b8db26 100644 --- a/app/books/page.tsx +++ b/app/books/page.tsx @@ -16,7 +16,7 @@ type ChapterEntry = { totalVerses: number }; type CantoEntry = { totalChapters: number; chapters: ChapterEntry[] }; type LilaEntry = { totalChapters: number; chapters: ChapterEntry[] }; -interface BgMeta extends BookMeta { +interface ChapterBookMeta extends BookMeta { totalChapters: number; chapters: ChapterEntry[]; } @@ -49,7 +49,7 @@ function buildBookList(allBooks: BookMeta[]): BookInfo[] { const author = data.author as string; if (slug === "bg") { - const bg = data as BgMeta; + const bg = data as ChapterBookMeta; const totalVerses = bg.chapters.reduce( (sum, ch) => sum + ch.totalVerses, 0, @@ -62,6 +62,20 @@ function buildBookList(allBooks: BookMeta[]): BookInfo[] { totalVerses, href: "/books/bg", }); + } else if (slug === "sb1962") { + const sb62 = data as ChapterBookMeta; + const totalVerses = sb62.chapters.reduce( + (sum, ch) => sum + ch.totalVerses, + 0, + ); + books.push({ + slug, + title, + author, + description: `${sb62.totalChapters} chapters · First Canto`, + totalVerses, + href: "/books/sb1962", + }); } else if (slug === "sb") { const sb = data as SbMeta; const totalChapters = sb.cantos.reduce( diff --git a/app/books/sb/[canto]/[chapter]/[verse]/page.tsx b/app/books/sb/[canto]/[chapter]/[verse]/page.tsx new file mode 100644 index 00000000..ed8156bf --- /dev/null +++ b/app/books/sb/[canto]/[chapter]/[verse]/page.tsx @@ -0,0 +1,83 @@ +import Link from "next/link"; +import { PageHeader } from "@/components/page-header"; +import { notFound } from "next/navigation"; +import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api"; +import { ChapterViewer } from "@/app/books/chapter-viewer"; + +type ChapterInfo = { + number: number; + title: string; + totalVerses: number; +}; + +type CantoInfo = { + number: number; + title: string; + totalChapters: number; + chapters: ChapterInfo[]; +}; + +export default async function SbVersePage({ + params, +}: { + params: Promise<{ canto: string; chapter: string; verse: string }>; +}) { + const { canto, chapter, verse } = await params; + + const meta = await getBookMeta("sb"); + if (!meta) notFound(); + + const cantos = (meta as unknown as { cantos: CantoInfo[] }).cantos; + const cantoInfo = cantos.find((c) => c.number === parseInt(canto)); + if (!cantoInfo) notFound(); + + const chapterInfo = cantoInfo.chapters.find( + (ch) => ch.number === parseInt(chapter), + ); + if (!chapterInfo) notFound(); + + const verseIndex = await getChapterVerseIndex("sb", canto, parseInt(chapter)); + if (verseIndex.length === 0) notFound(); + + const requestedVerse = parseInt(verse); + const verseData = await getSingleVerse( + "sb", + canto, + parseInt(chapter), + requestedVerse, + ); + if (!verseData) notFound(); + + return ( +
+ + + ← Canto {cantoInfo.number}: {cantoInfo.title} + +

+ Chapter {chapter}: {chapterInfo.title} +

+
+ +
+ ({ + verse: v.verse, + verseEnd: v.verseEnd ?? undefined, + reference: v.reference, + }))} + book="sb" + division={canto} + chapter={parseInt(chapter)} + showBengali={false} + basePath={`/books/sb/${canto}/${chapter}`} + /> +
+
+ ); +} diff --git a/app/books/sb/[canto]/[chapter]/page.tsx b/app/books/sb/[canto]/[chapter]/page.tsx index 2f259fe0..5003a70a 100644 --- a/app/books/sb/[canto]/[chapter]/page.tsx +++ b/app/books/sb/[canto]/[chapter]/page.tsx @@ -1,87 +1,16 @@ -import Link from "next/link"; -import { PageHeader } from "@/components/page-header"; +import { redirect } from "next/navigation"; import { notFound } from "next/navigation"; -import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api"; -import { ChapterViewer } from "@/app/books/chapter-viewer"; - -type ChapterInfo = { - number: number; - title: string; - totalVerses: number; -}; - -type CantoInfo = { - number: number; - title: string; - totalChapters: number; - chapters: ChapterInfo[]; -}; +import { getChapterVerseIndex } from "@/lib/api"; export default async function SbChapterPage({ params, - searchParams, }: { params: Promise<{ canto: string; chapter: string }>; - searchParams: Promise<{ verse?: string }>; }) { const { canto, chapter } = await params; - const { verse: verseParam } = await searchParams; - - const meta = await getBookMeta("sb"); - if (!meta) notFound(); - - const cantos = (meta as unknown as { cantos: CantoInfo[] }).cantos; - const cantoInfo = cantos.find((c) => c.number === parseInt(canto)); - if (!cantoInfo) notFound(); - - const chapterInfo = cantoInfo.chapters.find( - (ch) => ch.number === parseInt(chapter), - ); - if (!chapterInfo) notFound(); - const verseIndex = await getChapterVerseIndex("sb", canto, parseInt(chapter)); - if (verseIndex.length === 0) notFound(); - - const requestedVerse = verseParam - ? parseInt(verseParam) - : verseIndex[0].verse; - const verse = await getSingleVerse( - "sb", - canto, - parseInt(chapter), - requestedVerse, - ); - if (!verse) notFound(); - - return ( -
- - - ← Canto {cantoInfo.number}: {cantoInfo.title} - -

- Chapter {chapter}: {chapterInfo.title} -

-
- -
- ({ - verse: v.verse, - verseEnd: v.verseEnd ?? undefined, - reference: v.reference, - }))} - book="sb" - division={canto} - chapter={parseInt(chapter)} - showBengali={false} - /> -
-
- ); + if (verseIndex.length === 0) { + notFound(); + } + redirect(`/books/sb/${canto}/${chapter}/${verseIndex[0].verse}`); } diff --git a/app/books/sb1962/[chapter]/[verse]/page.tsx b/app/books/sb1962/[chapter]/[verse]/page.tsx new file mode 100644 index 00000000..c5494333 --- /dev/null +++ b/app/books/sb1962/[chapter]/[verse]/page.tsx @@ -0,0 +1,81 @@ +import Link from "next/link"; +import { PageHeader } from "@/components/page-header"; +import { notFound } from "next/navigation"; +import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api"; +import { ChapterViewer } from "@/app/books/chapter-viewer"; + +type Sb1962Meta = { + book: string; + title: string; + author: string; + totalChapters: number; + chapters: { number: number; title: string; totalVerses: number }[]; +}; + +export default async function VersePage({ + params, +}: { + params: Promise<{ chapter: string; verse: string }>; +}) { + const { chapter, verse: verseParam } = await params; + + const meta = await getBookMeta("sb1962"); + if (!meta) notFound(); + + const sb1962Meta = meta as unknown as Sb1962Meta; + const chapterInfo = sb1962Meta.chapters?.find( + (c) => c.number === parseInt(chapter), + ); + if (!chapterInfo?.title) notFound(); + + const title: string = chapterInfo.title; + + const verseIndex = await getChapterVerseIndex( + "sb1962", + null, + parseInt(chapter), + ); + if (verseIndex.length === 0) notFound(); + + const requestedVerse = parseInt(verseParam); + const verse = await getSingleVerse( + "sb1962", + null, + parseInt(chapter), + requestedVerse, + ); + if (!verse) notFound(); + + return ( +
+ + + ← Śrīmad-Bhāgavatam (1962) + +

+ Chapter {chapter}: {title} +

+
+ +
+ ({ + verse: v.verse, + verseEnd: v.verseEnd ?? undefined, + reference: v.reference, + }))} + book="sb1962" + division={null} + chapter={parseInt(chapter)} + basePath={`/books/sb1962/${chapter}`} + showBengali={false} + /> +
+
+ ); +} diff --git a/app/books/sb1962/[chapter]/page.tsx b/app/books/sb1962/[chapter]/page.tsx new file mode 100644 index 00000000..2484e869 --- /dev/null +++ b/app/books/sb1962/[chapter]/page.tsx @@ -0,0 +1,20 @@ +import { redirect } from "next/navigation"; +import { notFound } from "next/navigation"; +import { getChapterVerseIndex } from "@/lib/api"; + +export default async function ChapterPage({ + params, +}: { + params: Promise<{ chapter: string }>; +}) { + const { chapter } = await params; + const verseIndex = await getChapterVerseIndex( + "sb1962", + null, + parseInt(chapter), + ); + if (verseIndex.length === 0) { + notFound(); + } + redirect(`/books/sb1962/${chapter}/${verseIndex[0].verse}`); +} diff --git a/app/books/sb1962/page.tsx b/app/books/sb1962/page.tsx new file mode 100644 index 00000000..763426cb --- /dev/null +++ b/app/books/sb1962/page.tsx @@ -0,0 +1,69 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { getBookMeta } from "@/lib/api"; +import { PageHeader } from "@/components/page-header"; +import { Card, CardContent } from "@/components/ui/card"; +import { ExtractedVerseCount } from "../extracted-verse-count"; + +type ChapterInfo = { + number: number; + title: string; + totalVerses: number; +}; + +export default async function Sb1962Page() { + const meta = await getBookMeta("sb1962"); + if (!meta) notFound(); + + const intro = meta as unknown as { + book: string; + title: string; + author: string; + description?: string; + totalChapters: number; + chapters: ChapterInfo[]; + }; + + return ( +
+ + + ← Books + +

+ {intro.title} +

+

{intro.author}

+
+
+ + +
    + {intro.chapters.map((ch) => ( +
  • + +
    + + Chapter {ch.number} + +

    {ch.title}

    +
    + + + + +
  • + ))} +
+
+
+
+
+ ); +} diff --git a/scripts/__pycache__/sb_extract.cpython-313.pyc b/scripts/__pycache__/sb_extract.cpython-313.pyc index ac4ac6d4..5d1bda05 100644 Binary files a/scripts/__pycache__/sb_extract.cpython-313.pyc and b/scripts/__pycache__/sb_extract.cpython-313.pyc differ diff --git a/scripts/bg_extract.py b/scripts/bg_extract.py index 01ab682f..57f3fa88 100644 --- a/scripts/bg_extract.py +++ b/scripts/bg_extract.py @@ -389,7 +389,7 @@ def write_json(data, filepath): def main(): base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) html_path = os.path.join(base_dir, "public", "html", "bg.html") - output_dir = os.path.join(base_dir, "app", "lib", "books", "bg") + output_dir = os.path.join(base_dir, "lib", "jsons", "books", "bg") print(f"Reading {html_path}...") with open(html_path, "r", encoding="utf-8") as f: diff --git a/scripts/cc_extract.py b/scripts/cc_extract.py index 9c41df90..a499b34c 100644 --- a/scripts/cc_extract.py +++ b/scripts/cc_extract.py @@ -677,7 +677,7 @@ def write_json(data, filepath): def main(): base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) html_path = os.path.join(base_dir, "public", "html", "cc.html") - output_dir = os.path.join(base_dir, "app", "lib", "books", "cc") + output_dir = os.path.join(base_dir, "lib", "jsons", "books", "cc") print(f"Reading {html_path}...") with open(html_path, "r", encoding="utf-8") as f: diff --git a/scripts/glossary_extract.py b/scripts/glossary_extract.py index 465cdbe2..17dc5646 100644 --- a/scripts/glossary_extract.py +++ b/scripts/glossary_extract.py @@ -274,7 +274,7 @@ def build_letter_index(entries: list) -> dict: def main(): base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) html_path = os.path.join(base_dir, "public", "html", "glossary.html") - output_path = os.path.join(base_dir, "app", "lib", "glossary", "index.json") + output_path = os.path.join(base_dir, "lib", "jsons", "glossary", "index.json") if not os.path.exists(html_path): print(f"ERROR: {html_path} not found!") diff --git a/scripts/match_sb1962.py b/scripts/match_sb1962.py new file mode 100644 index 00000000..87d05c4a --- /dev/null +++ b/scripts/match_sb1962.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +"""Match SB 1962 transliterations and synonym words with standard SB Canto 1. + +For each verse in SB 1962 that has a matching verse in the standard SB +(same chapter + verse number), this script: + +1. Replaces the 1962 plain-English transliteration with the proper IAST version + from the standard SB. +2. Replaces each synonym "word" key with the IAST version from the standard SB, + matching by position (index). If the 1962 edition has MORE synonyms than the + standard SB, the extra ones are kept as-is. If the standard SB has more, + the extras are ignored. +3. Leaves the synonym "meanings" untouched (keeps the 1962 meanings). +4. Leaves the translation and purport untouched (keeps the 1962 text). +5. For verses that exist only in the 1962 edition (no standard SB match), + everything is left as-is. + +Usage: + python3 scripts/match_sb1962.py + +Prerequisites: + - Standard SB must be extracted at lib/jsons/books/sb/cantos/01/chapters/*/verses/*.json + - SB 1962 must be extracted at lib/jsons/books/sb-1962/chapters/*/verses/*.json +""" + +import glob +import json +import os + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SB_DIR = os.path.join(BASE_DIR, "lib", "jsons", "books", "sb", "cantos", "01") +SB62_DIR = os.path.join(BASE_DIR, "lib", "jsons", "books", "sb-1962") + +# Known misaligned verses where the 1962 numbering diverges from the standard SB. +# These are skipped during matching to avoid applying wrong IAST transliterations. +# The 1962 edition splits/numbers some verses differently in chapter 15. +SKIP_VERSES = { + (15, 41), + (15, 42), + (15, 43), + (15, 44), + (15, 45), +} + + +def normalize_transliteration(text: str) -> str: + """Normalize transliteration for fuzzy comparison. + + Strips diacritics, lowercases, removes punctuation/hyphens, collapses whitespace. + This allows matching 'Udichim pravibesasam' with 'udīcīṁ praviveśāśāṁ'. + """ + import unicodedata + + # NFD decompose then strip combining marks (diacritics) + nfkd = unicodedata.normalize("NFD", text) + stripped = "".join(c for c in nfkd if unicodedata.category(c) != "Mn") + # Lowercase, remove hyphens, punctuation, collapse whitespace + stripped = stripped.lower() + stripped = stripped.replace("-", "").replace("'", "").replace("'", "") + stripped = "".join(c for c in stripped if c.isalnum() or c.isspace()) + stripped = " ".join(stripped.split()) + return stripped + + +def transliteration_similarity(a: str, b: str) -> float: + """Compute a simple word-overlap similarity between two transliterations. + + Returns a ratio between 0.0 and 1.0. + """ + words_a = set(normalize_transliteration(a).split()) + words_b = set(normalize_transliteration(b).split()) + if not words_a or not words_b: + return 0.0 + intersection = words_a & words_b + union = words_a | words_b + return len(intersection) / len(union) + + +def load_sb_canto1(): + """Load all standard SB Canto 1 verses, keyed by (chapter, verse). + + Also returns a per-chapter list for fuzzy matching. + """ + verses = {} + by_chapter: dict[int, list] = {} + pattern = os.path.join(SB_DIR, "chapters", "*", "verses", "*.json") + for f in sorted(glob.glob(pattern)): + with open(f, "r", encoding="utf-8") as fh: + d = json.load(fh) + key = (d["chapter"], d["verse"]) + verses[key] = d + + ch = d["chapter"] + if ch not in by_chapter: + by_chapter[ch] = [] + by_chapter[ch].append(d) + + # For merged verses like 28-29, also index by the end verse + if "verse_end" in d and d["verse_end"]: + for v in range(d["verse"] + 1, d["verse_end"] + 1): + verses[(d["chapter"], v)] = d + return verses, by_chapter + + +def load_sb1962(): + """Load all SB 1962 verses, returning a list of (filepath, data) tuples.""" + results = [] + pattern = os.path.join(SB62_DIR, "chapters", "*", "verses", "*.json") + for f in sorted(glob.glob(pattern)): + with open(f, "r", encoding="utf-8") as fh: + d = json.load(fh) + results.append((f, d)) + return results + + +def match_synonyms(sb62_synonyms, sb_synonyms): + """Match synonym word keys from standard SB into the 1962 synonyms. + + Strategy: match by position index. For each synonym in the 1962 list, + if there's a corresponding synonym at the same index in the standard SB, + replace the word key with the IAST version. Keep the 1962 meaning. + + If the 1962 has more synonyms than the standard, keep the extras as-is. + """ + result = [] + for i, syn62 in enumerate(sb62_synonyms): + new_syn = dict(syn62) # copy + if i < len(sb_synonyms): + # Replace the word key with the IAST version + new_syn["word"] = sb_synonyms[i]["word"] + # Keep the original 1962 meaning regardless + result.append(new_syn) + return result + + +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) + + +def find_best_match(verse62: dict, sb_chapter_verses: list, threshold: float = 0.4): + """Find the best-matching standard SB verse by transliteration similarity. + + Used as a fallback when direct verse-number matching fails or produces + a low-confidence match. Returns the best-matching SB verse or None. + """ + trans_62 = verse62.get("transliteration", "") + if not trans_62.strip(): + return None + + best_score = 0.0 + best_match = None + for sb_verse in sb_chapter_verses: + sb_trans = sb_verse.get("transliteration", "") + score = transliteration_similarity(trans_62, sb_trans) + if score > best_score: + best_score = score + best_match = sb_verse + + if best_score >= threshold: + return best_match + return None + + +def main(): + print("Loading standard SB Canto 1 verses...") + sb_verses, sb_by_chapter = load_sb_canto1() + print(f" Loaded {len(sb_verses)} verse entries (including merged-verse aliases)") + + print("Loading SB 1962 verses...") + sb62_files = load_sb1962() + print(f" Loaded {len(sb62_files)} verse files") + + matched = 0 + unmatched = 0 + fuzzy_matched = 0 + trans_updated = 0 + syn_updated = 0 + + for filepath, verse62 in sb62_files: + ch = verse62["chapter"] + vs = verse62["verse"] + key = (ch, vs) + + # Skip known misaligned verses + if key in SKIP_VERSES: + unmatched += 1 + continue + + sb_verse = sb_verses.get(key) + + if sb_verse is None: + # No match — keep as-is (e.g., 1962-only verse numbers) + unmatched += 1 + continue + + matched += 1 + changed = False + + # 1. Replace transliteration with IAST version + sb_trans = sb_verse.get("transliteration", "").strip() + if sb_trans: + old_trans = verse62.get("transliteration", "") + verse62["transliteration"] = sb_trans + if old_trans != sb_trans: + trans_updated += 1 + changed = True + + # 2. Match synonym word keys + sb_syns = sb_verse.get("synonyms", []) + v62_syns = verse62.get("synonyms", []) + if sb_syns and v62_syns: + new_syns = match_synonyms(v62_syns, sb_syns) + if new_syns != v62_syns: + verse62["synonyms"] = new_syns + syn_updated += 1 + changed = True + + # 3. Write back if changed + if changed: + write_json(verse62, filepath) + + print(f"\n--- Results ---") + print(f"Total 1962 verses: {len(sb62_files)}") + print( + f"Matched with standard SB: {matched} (direct: {matched - fuzzy_matched}, fuzzy: {fuzzy_matched})" + ) + print(f"Unmatched (kept as-is): {unmatched}") + print(f"Transliterations updated: {trans_updated}") + print(f"Synonyms updated: {syn_updated}") + + # Show a sample of matched verses + print(f"\n--- Samples ---") + samples = [(1, 1), (1, 2), (2, 1), (7, 1), (13, 1)] + for ch, vs in samples: + fpath = os.path.join( + SB62_DIR, "chapters", str(ch).zfill(2), "verses", f"{str(vs).zfill(2)}.json" + ) + if os.path.exists(fpath): + with open(fpath, "r", encoding="utf-8") as f: + d = json.load(f) + print(f"\n {d['reference']}:") + trans_lines = d["transliteration"].split("\n") + print(f" Transliteration: {trans_lines[0]}") + if len(trans_lines) > 1: + print(f" {trans_lines[1]}") + if d["synonyms"]: + print(f" First synonym: {d['synonyms'][0]}") + + # Show unmatched verses + if unmatched > 0: + print(f"\n--- Unmatched 1962 verses (kept as-is) ---") + for filepath, verse62 in sb62_files: + ch = verse62["chapter"] + vs = verse62["verse"] + if (ch, vs) not in sb_verses: + print(f" {verse62['reference']}: {verse62['transliteration'][:50]}...") + + +if __name__ == "__main__": + main() diff --git a/scripts/sb1962_extract.py b/scripts/sb1962_extract.py new file mode 100644 index 00000000..93d312c5 --- /dev/null +++ b/scripts/sb1962_extract.py @@ -0,0 +1,617 @@ +#!/usr/bin/env python3 +"""Extract 1962 Śrīmad-Bhāgavatam First Canto verses from others-before-1977.html. + +This is the original 1962-65 edition of Śrīmad-Bhāgavatam First Canto published +in India before Śrīla Prabhupāda went to the USA. It spans 3 volumes: + - Volume 1 (1962): Chapters 1–7 (first half) + - Volume 2 (1964): Chapter 7 (second half) through Chapter 12 + - Volume 3 (1965): Chapters 13–19 + +Key differences from the standard SB extraction: + - No Devanagari or ScaGoudy encoding — transliteration is plain English (non-IAST) + - Different CSS class mappings + - Verse markers use "TEXT No. N" format (p18 class) + - Reference markers (p17) are sparse — only some verses have them + - Chapter boundaries use spelled-out ordinals: "FIRST CHAPTER", "SECOND CHAPTER", etc. + - Multi-verse entries use "TEXTS No.'s 3 AND 4" format + +Output goes to app/lib/books/sb1962/ +""" + +import json +import os +import re +import sys + +from bs4 import BeautifulSoup, Tag + +# --------------------------------------------------------------------------- +# Chapter name → number mapping +# --------------------------------------------------------------------------- + +ORDINAL_TO_NUM = { + "first": 1, + "second": 2, + "third": 3, + "fourth": 4, + "fifth": 5, + "sixth": 6, + "seventh": 7, + "eighth": 8, + "nineth": 9, # sic — typo in the original + "ninth": 9, + "tenth": 10, + "eleventh": 11, + "twelfth": 12, + "thirteenth": 13, + "fourteenth": 14, + "fifteenth": 15, + "sixteenth": 16, + "seventeenth": 17, + "eighteenth": 18, + "nineteenth": 19, +} + +# Chapter titles (from the "Thus end" markers and chapter headers) +CHAPTER_TITLES = { + 1: "Questions by the Sages", + 2: "Divinity and Devotional Service", + 3: "Description of Incarnations of Godhead", + 4: "Appearance of Sri Narada", + 5: "Sri Narada's Instructions on Srimad Bhagwatam for Vyasdeva", + 6: "Conversation Between Narada and Vyasa", + 7: "The Son of Drona Punished", + 8: "Prayers of Queen Kunti and Parikshit Saved", + 9: "Passing Away of Bhismadeva in the Presence of Lord Krishna", + 10: "Departure of Lord Krishna for Dwarka", + 11: "Lord Krishna's Entrance at Dwarka", + 12: "Birth of Maharaj Parikshit", + 13: "Dhritarastra Quits Home", + 14: "Disappearance of Lord Krishna", + 15: "The Pandavas Retire Timely", + 16: "How Parikshit Received The Age of Kali", + 17: "Punishment and Reward of Kali", + 18: "Maharaj Parikshit Cursed by a Brahmin Boy", + 19: "Appearance of Sri Sukdeva Goswami", +} + + +# --------------------------------------------------------------------------- +# HTML helpers +# --------------------------------------------------------------------------- + + +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 decode_html_content(tag: Tag) -> str: + """Extract inner HTML preserving / formatting.""" + 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) + inner = re.sub(r"\s+", " ", inner).strip() + return inner + + +def decode_text(tag: Tag) -> str: + """Extract plain text from a tag.""" + return tag.get_text().strip() + + +# --------------------------------------------------------------------------- +# Synonym parser +# --------------------------------------------------------------------------- + + +def parse_synonyms(text: str) -> list: + """Parse synonym text from the 1962 edition. + + Format is similar to later editions but uses plain English: + "Om—Oh my Lord, namo—offering my obeisances, ..." + Words may be wrapped in tags. + """ + # Strip HTML tags for parsing + plain = re.sub(r"<[^>]+>", "", text).strip() + if not plain: + return [] + + result = [] + # Split on comma that's followed by an italic word (the next synonym) + # But also handle semicolons + # The pattern is: word—meaning, word—meaning, ... + # Split on ", " followed by what looks like a new word entry + # Actually simpler: split on the em-dash pattern + entries = re.split(r",\s*(?=[A-Z])", plain) + + 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) + elif "- " in entry: + # Some entries use hyphen-space + parts = entry.split("- ", 1) + else: + continue + + if len(parts) == 2: + word = parts[0].strip().strip("., ;:") + meaning = parts[1].strip().rstrip(".,") + if word and meaning: + result.append({"word": word, "meanings": [meaning]}) + + return result + + +# --------------------------------------------------------------------------- +# CSS class sets for the 1962 SB +# --------------------------------------------------------------------------- + +# Verse reference bookmark (sparse — not all verses have these) +VERSE_REF_CLASS = "p17" + +# TEXT No. N header +TEXT_HEADER_CLASS = "p18" + +# "TEXT" label +TEXT_LABEL_CLASS = "p2" + +# Transliteration pāda lines (italic, centered) +TRANSLITERATION_CLASSES = frozenset(["p19", "p87"]) + +# Section header class: "ENGLISH SYNONYMS", "TRANSLATION", "PURPORT" +SECTION_HEADER_CLASSES = frozenset(["p20", "p8"]) + +# Synonyms text class +SYNONYM_CLASSES = frozenset(["p88"]) + +# Translation text class +TRANSLATION_CLASSES = frozenset(["p89"]) + +# Purport paragraph classes +PURPORT_CLASSES = frozenset(["p74", "p6", "p48"]) + +# Chapter heading class +CHAPTER_HEADING_CLASS = "p3" + +# Chapter bookmark class +CHAPTER_BOOKMARK_CLASS = "p11" + +# Volume/section boundary classes — skip +SKIP_CLASSES = frozenset( + [ + "p35", # volume title + "p8", # various front matter and banners (also used for some headers) + "p73", # "First Published in 1962" + "p105", # chapter subtitle in volume 2 + "p106", # chapter subtitle in volume 3 + "p71", # review quotes + "p72", # review quotes + ] +) + +# Line range in the HTML for the SB 1962 content +# From the first chapter marker to "END OF THE FIRST CANTO" +SB1962_START_LINE = 4726 +SB1962_END_LINE = 15162 + + +# --------------------------------------------------------------------------- +# Verse number parser +# --------------------------------------------------------------------------- + + +def parse_text_header(text: str): + """Parse a TEXT header like 'TEXT No. 1' or 'TEXTS No.'s 3 AND 4'. + + Returns (verse_num, verse_end) or None. + """ + text = text.strip() + + # Multi-verse: "TEXTS No.'s 3 AND 4" or "TEXTS Nos. 26 AND 27" + m = re.match( + r"TEXTS?\s+No\.?'?s?\s*(\d+)\s+AND\s+(\d+)", + text, + re.IGNORECASE, + ) + if m: + return (int(m.group(1)), int(m.group(2))) + + # Single verse: "TEXT No. 1" or "TEXT No. 1." + m = re.match(r"TEXT\s+No\.?\s*(\d+)", text, re.IGNORECASE) + if m: + return (int(m.group(1)), None) + + return None + + +def parse_chapter_heading(text: str): + """Parse a chapter heading like 'FIRST CHAPTER' or 'SEVENTH CHAPTER'. + + Returns chapter number or None. + """ + text = text.strip().upper() + # Remove bold/italic artifacts + text = re.sub(r"<[^>]+>", "", text).strip() + + m = re.match(r"(\w+)\s+CHAPTER", text) + if m: + ordinal = m.group(1).lower() + return ORDINAL_TO_NUM.get(ordinal) + return None + + +def parse_ref_marker(text: str): + """Parse a reference marker like 'SB. 1-1962:1.1.1' or 'SB. 1.13-1965:1.13.3-4'. + + Returns (chapter, verse, verse_end) or None. + """ + # Match patterns like: SB. 1-1962:1.1.1 or SB. 1.7-1964:1.7.9 or SB. 1.13-1965:1.13.3-4 + m = re.search(r"SB\.\s*[\d.]+-\d{4}:1\.(\d+)\.(\d+)(?:-(\d+))?", text) + if m: + chapter = int(m.group(1)) + verse = int(m.group(2)) + verse_end = int(m.group(3)) if m.group(3) else None + return (chapter, verse, verse_end) + return None + + +# --------------------------------------------------------------------------- +# State machine extraction +# --------------------------------------------------------------------------- + + +def _finalize_verse(verse: dict): + """Clean up a verse before appending to results.""" + # Join transliteration lines + padas = verse.pop("_padas", []) + verse["transliteration"] = "\n".join(padas) + + # Clean up internal state + verse.pop("_state", None) + + +def extract_verses(soup: BeautifulSoup): + """Extract all SB 1962 verses from the HTML. + + Returns (verses, chapter_count). + """ + all_paragraphs = soup.find_all("p") + + # Filter to only the paragraphs in the SB 1962 range + # We use the sourceline attribute if available, otherwise fall back to + # scanning for the start/end markers + paragraphs = [] + in_range = False + + for p in all_paragraphs: + cls = get_css_class(p) + text = decode_text(p) + + # Start: first chapter marker + if cls == CHAPTER_BOOKMARK_CLASS and "SB. 1-1962:: First Chapter" in text: + in_range = True + + if in_range: + paragraphs.append(p) + + # End: "END OF THE FIRST CANTO" + if in_range and "END OF THE FIRST CANTO" in text: + break + + if not paragraphs: + print("ERROR: Could not find SB 1962 content in the HTML!") + return [], 0 + + print(f" Found {len(paragraphs)} paragraphs in SB 1962 range") + + verses = [] + current_verse = None + state = "IDLE" + current_chapter = 0 + chapters_seen = set() + + for p in paragraphs: + cls = get_css_class(p) + text = decode_text(p) + + # --- Chapter heading (p3): "FIRST CHAPTER", "SECOND CHAPTER", etc. --- + if cls == CHAPTER_HEADING_CLASS: + ch_num = parse_chapter_heading(text) + if ch_num is not None: + # Finalize previous verse if any + if current_verse: + _finalize_verse(current_verse) + verses.append(current_verse) + current_verse = None + state = "IDLE" + + current_chapter = ch_num + chapters_seen.add(ch_num) + continue + + # Skip non-chapter headings (APPENDIX, CONTENTS, PREFACE, etc.) + if any( + kw in text.upper() + for kw in ["APPENDIX", "CONTENTS", "PREFACE", "ERRATA"] + ): + # We've left the chapter content — stop processing until next chapter + if current_verse: + _finalize_verse(current_verse) + verses.append(current_verse) + current_verse = None + state = "SKIP" + continue + + # --- Skip mode (appendix, front matter between volumes) --- + if state == "SKIP": + # Resume when we hit a new chapter heading + if cls == CHAPTER_HEADING_CLASS: + ch_num = parse_chapter_heading(text) + if ch_num is not None: + current_chapter = ch_num + chapters_seen.add(ch_num) + state = "IDLE" + continue + # Also resume on a verse ref marker for the next volume + if cls == VERSE_REF_CLASS and "SB." in text: + ref = parse_ref_marker(text) + if ref: + state = "IDLE" + # Fall through to handle the ref below + else: + continue + else: + continue + + # --- Chapter bookmark (p11) — might give us the chapter number --- + if cls == CHAPTER_BOOKMARK_CLASS: + # Could be "SB. 1-1962:: First Chapter" or "SB. 1.7-1964: ..." + # We already handle chapter number from the p3 heading + continue + + # --- Volume/section boundaries — skip --- + if cls in SKIP_CLASSES: + continue + + # --- Verse reference marker (p17) --- + if cls == VERSE_REF_CLASS: + ref = parse_ref_marker(text) + if ref: + # This gives us chapter/verse info, but the TEXT header is the + # primary verse trigger. Just note the chapter if different. + ch, vs, vs_end = ref + if ch != current_chapter and ch > 0: + current_chapter = ch + chapters_seen.add(ch) + continue + + # --- TEXT No. N header (p18) — primary verse trigger --- + if cls == TEXT_HEADER_CLASS: + parsed = parse_text_header(text) + if parsed is None: + continue + + verse_num, verse_end = parsed + + # Finalize previous verse + if current_verse: + _finalize_verse(current_verse) + verses.append(current_verse) + + if verse_end: + ref_str = f"SB 1.{current_chapter}.{verse_num}-{verse_end}" + else: + ref_str = f"SB 1.{current_chapter}.{verse_num}" + + current_verse = { + "book_title": "Śrīmad-Bhāgavatam (1962 Edition)", + "chapter": current_chapter, + "verse": verse_num, + "reference": ref_str, + "transliteration": "", + "_padas": [], + "synonyms": [], + "translation": "", + "purport": [], + "_state": "VERSE_START", + } + if verse_end is not None: + current_verse["verse_end"] = verse_end + state = "VERSE_START" + continue + + # --- TEXT label (p2) — just confirms we're in a verse --- + if cls == TEXT_LABEL_CLASS and current_verse: + continue + + # --- Transliteration lines (p19, p87) --- + if ( + cls in TRANSLITERATION_CLASSES + and current_verse + and state in ("VERSE_START", "TRANSLITERATION") + ): + state = "TRANSLITERATION" + current_verse["_state"] = state + line = decode_text(p) + if line: + current_verse["_padas"].append(line) + continue + + # --- Section headers (p20, p8): ENGLISH SYNONYMS, TRANSLATION, PURPORT --- + if cls in SECTION_HEADER_CLASSES and current_verse: + header_text = text.strip().upper() + if "SYNONYM" in header_text: + state = "SYNONYMS" + current_verse["_state"] = state + elif "TRANSLATION" in header_text: + state = "TRANSLATION" + current_verse["_state"] = state + elif "PURPORT" in header_text: + state = "PURPORT" + current_verse["_state"] = state + continue + + # --- Synonyms text (p88) --- + if cls in SYNONYM_CLASSES and current_verse and state == "SYNONYMS": + html_content = decode_html_content(p) + current_verse["synonyms"] = parse_synonyms(html_content) + continue + + # --- Translation text (p89) --- + if cls in TRANSLATION_CLASSES and current_verse and state == "TRANSLATION": + current_verse["translation"] = decode_html_content(p) + state = "POST_TRANSLATION" + current_verse["_state"] = state + continue + + # --- Handle mislabeled headers (TRANSLATION header followed by + # translation in p89 while state is PURPORT) --- + if ( + cls in TRANSLATION_CLASSES + and current_verse + and not current_verse.get("translation") + ): + current_verse["translation"] = decode_html_content(p) + state = "POST_TRANSLATION" + current_verse["_state"] = state + continue + + # --- Purport paragraphs (p74, p6, p48) --- + if state == "PURPORT" and current_verse and cls in PURPORT_CLASSES: + content = decode_html_content(p) + if content and "Thus end" not in text and "Thus ended" not in text: + current_verse["purport"].append(content) + continue + + # --- Chapter end marker ("Thus end the Bhaktivedanta Purports") --- + if ("Thus end" in text or "Thus ended" in text) and current_verse: + _finalize_verse(current_verse) + verses.append(current_verse) + current_verse = None + state = "IDLE" + continue + + # Don't forget the last verse + if current_verse: + _finalize_verse(current_verse) + verses.append(current_verse) + + return verses, len(chapters_seen) + + +# --------------------------------------------------------------------------- +# JSON output +# --------------------------------------------------------------------------- + + +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}") + + +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", "books", "sb-1962") + + 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 SB 1962 verses...") + verses, chapter_count = extract_verses(soup) + + print(f"\nExtracted {len(verses)} verses across {chapter_count} chapters") + + # Group by chapter + chapters_info = {} + for verse in verses: + ch = verse["chapter"] + if ch not in chapters_info: + chapters_info[ch] = 0 + chapters_info[ch] += 1 + + # Build intro.json + chapters_list = [] + for ch_num in sorted(chapters_info.keys()): + chapters_list.append( + { + "number": ch_num, + "title": CHAPTER_TITLES.get(ch_num, f"Chapter {ch_num}"), + "totalVerses": chapters_info[ch_num], + } + ) + + intro = { + "book": "sb1962", + "title": "Śrīmad-Bhāgavatam — First Canto (1962 Edition)", + "author": "His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda", + "description": "The original 1962-65 Indian edition of the First Canto, published before Śrīla Prabhupāda's journey to the West. Three volumes covering 19 chapters.", + "totalChapters": len(chapters_list), + "chapters": chapters_list, + } + write_json(intro, os.path.join(output_dir, "intro.json")) + + # Write verse JSON files + for verse in verses: + ch = str(verse["chapter"]).zfill(2) + vs = str(verse["verse"]).zfill(2) + filepath = os.path.join(output_dir, "chapters", ch, "verses", f"{vs}.json") + write_json(verse, filepath) + + # Print summary + print("\n--- Verification ---") + for ch_num in sorted(chapters_info.keys()): + ch_verses = [v for v in verses if v["chapter"] == ch_num] + if ch_verses: + first = ch_verses[0] + last = ch_verses[-1] + title = CHAPTER_TITLES.get(ch_num, "") + print( + f" Ch {ch_num:>2}: {len(ch_verses):>3} verses " + f"({first['reference']} → {last['reference']}) {title}" + ) + + # Quick quality check + if verses: + print("\n--- Sample (first verse) ---") + v = verses[0] + print(f"Reference: {v['reference']}") + print(f"Transliteration:\n{v['transliteration']}") + print(f"Synonyms: {len(v['synonyms'])} entries") + if v["synonyms"]: + print(f" First: {v['synonyms'][0]}") + trans = v["translation"][:120] if v["translation"] else "(none)" + print(f"Translation: {trans}...") + print(f"Purport: {len(v['purport'])} paragraphs") + + # Quality stats + print("\n--- Quality Stats ---") + empty_trans = sum(1 for v in verses if not v.get("transliteration", "").strip()) + empty_translation = sum(1 for v in verses if not v.get("translation", "").strip()) + empty_syn = sum(1 for v in verses if not v.get("synonyms")) + has_purport = sum(1 for v in verses if v.get("purport")) + print(f"Total verses: {len(verses)}") + print(f"Empty transliteration: {empty_trans}") + print(f"Empty translation: {empty_translation}") + print(f"Empty synonyms: {empty_syn}") + print(f"Verses with purport: {has_purport}") + print(f"Verses without purport: {len(verses) - has_purport}") + + +if __name__ == "__main__": + main() diff --git a/scripts/sb_extract.py b/scripts/sb_extract.py index ab61a212..89948d9d 100644 --- a/scripts/sb_extract.py +++ b/scripts/sb_extract.py @@ -555,8 +555,38 @@ def extract_verses(soup: BeautifulSoup, max_canto=99, max_chapter=999, max_verse if state == "SUMMARY": continue - # --- TEXT N header (p21) --- + # --- TEXT N header (p21) — authoritative verse number --- + # The p20 reference sometimes has wrong verse numbers (duplicates). + # The p21 TEXT header always has the correct verse number. if cls == TEXT_HEADER_CLASS and current_verse: + m = re.match(r"TEXTS?\s+(\d+)(?:\s*[-–]\s*(\d+))?", text, re.IGNORECASE) + if m: + correct_verse = int(m.group(1)) + correct_end = int(m.group(2)) if m.group(2) else None + + # Fix the verse number if p20 was wrong + if current_verse["verse"] != correct_verse: + current_verse["verse"] = correct_verse + + # Fix verse_end + if correct_end is not None: + current_verse["verse_end"] = correct_end + elif ( + "verse_end" in current_verse + and current_verse.get("verse_end") != correct_end + ): + if correct_end is None and "verse_end" in current_verse: + del current_verse["verse_end"] + + # Rebuild the clean reference with corrected numbers + canto = current_verse["canto"] + chapter = current_verse["chapter"] + if correct_end is not None: + current_verse["reference"] = ( + f"SB {canto}.{chapter}.{correct_verse}-{correct_end}" + ) + else: + current_verse["reference"] = f"SB {canto}.{chapter}.{correct_verse}" continue # --- TEXT label (p22) --- @@ -691,7 +721,7 @@ def write_json(data, filepath): def main(): base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) html_path = os.path.join(base_dir, "public", "html", "sb.html") - output_dir = os.path.join(base_dir, "app", "lib", "books", "sb") + output_dir = os.path.join(base_dir, "lib", "jsons", "books", "sb") print(f"Reading {html_path}...") with open(html_path, "r", encoding="utf-8") as f: diff --git a/scripts/seed-db.ts b/scripts/seed-db.ts index 815b32ec..9c937638 100644 --- a/scripts/seed-db.ts +++ b/scripts/seed-db.ts @@ -30,7 +30,7 @@ if (!DATABASE_URL) { } const BASE_DIR = path.resolve(__dirname, ".."); -const LIB_DIR = path.join(BASE_DIR, "app", "lib"); +const LIB_DIR = path.join(BASE_DIR, "lib", "jsons"); // --------------------------------------------------------------------------- // Helpers @@ -95,6 +95,18 @@ type CcVerse = { purport: string[]; }; +type Sb1962Verse = { + book_title: string; + chapter: number; + verse: number; + verse_end?: number; + reference: string; + transliteration: string; + synonyms: { word: string; meanings: string[] }[]; + translation: string; + purport: string[]; +}; + type GlossaryData = { title: string; description: string; @@ -328,6 +340,69 @@ async function seedCc(db: ReturnType) { console.log(` ✅ CC: ${totalCount} verses`); } +// --------------------------------------------------------------------------- +// SB 1962 +// --------------------------------------------------------------------------- + +async function seedSb1962(db: ReturnType) { + const introPath = path.join(LIB_DIR, "books", "sb-1962", "intro.json"); + if (!fs.existsSync(introPath)) { + console.log(" ⏭ SB 1962 intro.json not found, skipping"); + return; + } + + const intro = readJson(introPath); + + const metaCol = db.collection("book_meta"); + await metaCol.deleteMany({ book: "sb1962" }); + await metaCol.insertOne({ book: "sb1962", data: intro }); + console.log(" 📖 SB 1962 book meta inserted"); + + const versesCol = db.collection("verses"); + await versesCol.deleteMany({ book: "sb1962" }); + + const chaptersDir = path.join(LIB_DIR, "books", "sb-1962", "chapters"); + if (!fs.existsSync(chaptersDir)) { + console.log(" ⏭ SB 1962 chapters dir not found, skipping verses"); + return; + } + + const chapterDirs = fs + .readdirSync(chaptersDir) + .filter((d) => fs.statSync(path.join(chaptersDir, d)).isDirectory()) + .sort(); + + const docs: Record[] = []; + + for (const chDir of chapterDirs) { + const versesDir = path.join(chaptersDir, chDir, "verses"); + const verses = readAllJsonInDir(versesDir) as Sb1962Verse[]; + + for (const v of verses) { + docs.push({ + book: "sb1962", + division: null, + chapter: v.chapter, + verse: v.verse, + verseEnd: v.verse_end ?? null, + reference: v.reference, + bookTitle: v.book_title, + devanagari: "", + bengali: null, + transliteration: v.transliteration, + synonyms: v.synonyms, + translation: v.translation, + purport: v.purport, + }); + } + } + + if (docs.length > 0) { + await versesCol.insertMany(docs, { ordered: false }); + } + console.log(` ✅ SB 1962: ${docs.length} verses`); +} + // --------------------------------------------------------------------------- // Glossary // --------------------------------------------------------------------------- @@ -396,23 +471,27 @@ async function main() { const start = Date.now(); - console.log("[1/5] Bhagavad-gītā"); + console.log("[1/6] Bhagavad-gītā"); await seedBg(db); console.log(); - console.log("[2/5] Śrīmad-Bhāgavatam"); + console.log("[2/6] Śrīmad-Bhāgavatam"); await seedSb(db); console.log(); - console.log("[3/5] Śrī Caitanya-caritāmṛta"); + console.log("[3/6] Śrī Caitanya-caritāmṛta"); await seedCc(db); console.log(); - console.log("[4/5] Glossary"); + console.log("[4/6] Śrīmad-Bhāgavatam (1962 Edition)"); + await seedSb1962(db); + console.log(); + + console.log("[5/6] Glossary"); await seedGlossary(db); console.log(); - console.log("[5/5] Indexes"); + console.log("[6/6] Indexes"); await createIndexes(db); console.log();