mongo fix

This commit is contained in:
gosangam
2026-04-14 19:12:11 +05:30
parent e2f8063d90
commit beffb9fae1
172 changed files with 18029 additions and 1298 deletions
+3
View File
@@ -34,6 +34,9 @@ SCAGOUDY_TO_IAST = {
"É": "Ī",
"à": "ṁ",
"ù": "ḥ",
"è": "ṝ", # long vocalic r (U+1E5D)
"û": "ṁl", # anusvara + l (ScaGoudy ligature glyph)
"ÿ": "ḷ", # vocalic l (U+1E37)
}
+3
View File
@@ -44,6 +44,9 @@ SCAGOUDY_TO_IAST = {
"É": "Ī",
"à": "ṁ",
"ù": "ḥ",
"è": "ṝ", # long vocalic r (U+1E5D)
"û": "ṁl", # anusvara + l (ScaGoudy ligature glyph)
"ÿ": "ḷ", # vocalic l (U+1E37)
}
+335
View File
@@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""Extract glossary entries from glossary.html into structured JSON.
The glossary HTML contains:
1. A main consolidated glossary (p3/p4 class entries, ~2500 entries)
Format: <span class="s1">Term—</span>Definition text
Or: Term—<span class="s3">Definition text</span>
2. Book-specific glossaries (p10/p25/p26/p22 etc.) that are subsets
We extract ALL unique entries, merging duplicates by preferring the longer
definition. Output goes to app/lib/glossary/index.json.
Usage:
python3 scripts/glossary_extract.py
"""
import json
import os
import re
import sys
from bs4 import BeautifulSoup, Tag
# ---------------------------------------------------------------------------
# ScaGoudy → IAST Unicode mapping (shared with other extract scripts)
# ---------------------------------------------------------------------------
SCAGOUDY_TO_IAST = {
"ä": "ā",
"Ä": "Ā",
"å": "ṛ",
"Å": "Ṛ",
"ñ": "ṣ",
"Ñ": "Ṣ",
"ö": "ṭ",
"Ö": "Ṭ",
"ë": "ṇ",
"Ë": "Ṇ",
"ò": "ḍ",
"Ò": "Ḍ",
"ç": "ś",
"Ç": "Ś",
"ì": "ṅ",
"ï": "ñ",
"ü": "ū",
"Ü": "Ū",
"é": "ī",
"É": "Ī",
"à": "ṁ",
"ù": "ḥ",
"è": "ṝ", # long vocalic r (U+1E5D)
"û": "ṁl", # anusvara + l (ScaGoudy ligature glyph)
"ÿ": "ḷ", # vocalic l (U+1E37)
}
def decode_scagoudy(text: str) -> str:
"""Convert ScaGoudy-encoded text to proper IAST Unicode."""
for src, dst in SCAGOUDY_TO_IAST.items():
text = text.replace(src, dst)
return text
def decode_html_content(tag: Tag) -> str:
"""Extract inner HTML from a tag, decode ScaGoudy, preserve <i>/<b> formatting."""
inner = "".join(str(child) for child in tag.children)
inner = decode_scagoudy(inner)
# Remove span tags with CSS classes (RTF conversion artifacts)
inner = re.sub(r'<span[^>]*class="[^"]*"[^>]*>(.*?)</span>', r"\1", inner)
inner = re.sub(r"\s+", " ", inner).strip()
return inner
def decode_text(tag: Tag) -> str:
"""Extract plain text from a tag and decode ScaGoudy."""
text = tag.get_text()
return decode_scagoudy(text).strip()
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 ""
# ---------------------------------------------------------------------------
# Entry classes
# ---------------------------------------------------------------------------
# Main glossary entry classes (consolidated glossary at the top)
MAIN_ENTRY_CLASSES = frozenset(["p3", "p4"])
# Book-specific glossary entry classes (smaller per-book glossaries below)
BOOK_ENTRY_CLASSES = frozenset(["p10", "p22", "p24", "p25", "p26", "p27", "p19", "p34"])
# Letter heading classes (A, B, C...) — skip
LETTER_HEADING_CLASSES = frozenset(
["p9", "p12", "p14", "p15", "p23", "p29", "p30", "p31"]
)
# Section heading classes (book titles, "ABOUT THE AUTHOR", etc.) — skip
SECTION_HEADING_CLASSES = frozenset(["p7", "p17", "p18", "p2"])
# Skip classes (whitespace, decorative, front matter)
SKIP_CLASSES = frozenset(["p1", "p6", "p8", "p11", "p13", "p16", "p28", "p32", "p33"])
def parse_entry_p3(tag: Tag):
"""Parse a main glossary entry (p3/p4 class).
Format: <span class="s1">Term—</span>Definition
Or: <span class="s1">Term</span>—Definition (sometimes the dash is outside)
Or: Term—<span class="s3">definition</span> (p4 class)
Returns (term, definition_html) or None.
"""
html = decode_html_content(tag)
if not html:
return None
# The term and definition are separated by an em-dash (—)
# Sometimes there's an italic dash: <i>—</i>
# Normalize italic dashes
html = re.sub(r"<i>\s*—\s*</i>", "—", html)
# Split on the FIRST em-dash
if "—" not in html:
return None
idx = html.index("—")
term_part = html[:idx].strip()
definition_part = html[idx + 1 :].strip()
# Clean up the term: strip HTML tags, trailing/leading punctuation
term = re.sub(r"<[^>]+>", "", term_part).strip()
term = term.strip("., ;:")
if not term or not definition_part:
return None
return (term, definition_part)
def parse_entry_book(tag: Tag):
"""Parse a book-specific glossary entry (p10/p25/p26 etc.).
Format: <i>Term</i>—definition
Or: <b>Term</b>—definition
Or: Term—definition (plain text)
Returns (term, definition_html) or None.
"""
html = decode_html_content(tag)
if not html:
return None
# Normalize italic dashes
html = re.sub(r"<i>\s*—\s*</i>", "—", html)
if "—" not in html:
return None
idx = html.index("—")
term_part = html[:idx].strip()
definition_part = html[idx + 1 :].strip()
term = re.sub(r"<[^>]+>", "", term_part).strip()
term = term.strip("., ;:")
if not term or not definition_part:
return None
return (term, definition_part)
def strip_html(html: str) -> str:
"""Strip all HTML tags from a string, returning plain text."""
return re.sub(r"<[^>]+>", "", html).strip()
def extract_glossary(soup: BeautifulSoup):
"""Extract all glossary entries from the parsed HTML.
Returns a dict: { normalized_term: { "term": str, "definition": str, "definition_html": str } }
Duplicates are merged by keeping the longer definition.
"""
entries = {}
for p in soup.find_all("p"):
cls = get_css_class(p)
# Skip non-entry paragraphs
if cls in LETTER_HEADING_CLASSES:
continue
if cls in SECTION_HEADING_CLASSES:
continue
if cls in SKIP_CLASSES:
continue
parsed = None
if cls in MAIN_ENTRY_CLASSES:
parsed = parse_entry_p3(p)
elif cls in BOOK_ENTRY_CLASSES:
parsed = parse_entry_book(p)
else:
# Try parsing unknown classes as potential entries
text = decode_text(p)
if "—" in text and len(text) > 5:
parsed = parse_entry_book(p)
if parsed is None:
continue
term, definition_html = parsed
definition_plain = strip_html(definition_html)
# Normalize the term for deduplication (lowercase, strip diacritics for matching)
norm_key = term.lower().strip()
if norm_key in entries:
# Keep the longer definition
existing = entries[norm_key]
if len(definition_plain) > len(strip_html(existing["definition_html"])):
entries[norm_key] = {
"term": term,
"definition": definition_plain,
"definition_html": definition_html,
}
else:
entries[norm_key] = {
"term": term,
"definition": definition_plain,
"definition_html": definition_html,
}
return entries
def build_letter_index(entries: list) -> dict:
"""Group entries by their first letter for alphabetical navigation."""
index = {}
for entry in entries:
term = entry["term"]
# Get the first real letter (skip diacritics for grouping)
first = term[0].upper() if term else "?"
# Normalize accented first letters to base letter for grouping
# Ā→A, Ś→S, etc.
base_map = {
"Ā": "A",
"Ä": "A",
"Ī": "I",
"É": "I",
"Ū": "U",
"Ü": "U",
"Ṛ": "R",
"Ṝ": "R",
"Ś": "S",
"Ç": "S",
"Ṣ": "S",
"Ñ": "N",
"Ṇ": "N",
"Ṅ": "N",
"Ṭ": "T",
"Ḍ": "D",
"Ḥ": "H",
}
letter = base_map.get(first, first)
if letter not in index:
index[letter] = []
index[letter].append(entry)
return index
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")
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 glossary entries...")
entries_dict = extract_glossary(soup)
# Convert to sorted list
entries_list = sorted(entries_dict.values(), key=lambda e: e["term"].lower())
print(f"Extracted {len(entries_list)} unique glossary entries")
# Build letter index
letter_index = build_letter_index(entries_list)
# Build output JSON
output = {
"title": "Glossary of Vedic Terms",
"description": "A comprehensive glossary of Sanskrit and Vedic terms from the books of His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda.",
"totalEntries": len(entries_list),
"letters": sorted(letter_index.keys()),
"entries": entries_list,
}
# Write JSON
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(output, f, ensure_ascii=False, indent=2)
print(f"Written: {output_path}")
# Print summary
print(f"\n--- Summary ---")
print(f"Total unique entries: {len(entries_list)}")
print(f"Letters: {', '.join(sorted(letter_index.keys()))}")
print(f"\nEntries per letter:")
for letter in sorted(letter_index.keys()):
count = len(letter_index[letter])
print(f" {letter}: {count}")
# Show first few entries as a sample
print(f"\n--- Sample entries ---")
for entry in entries_list[:5]:
defn = entry["definition"][:80]
print(f" {entry['term']}: {defn}...")
print(f"\n--- Last few entries ---")
for entry in entries_list[-3:]:
defn = entry["definition"][:80]
print(f" {entry['term']}: {defn}...")
if __name__ == "__main__":
main()
+3
View File
@@ -44,6 +44,9 @@ SCAGOUDY_TO_IAST = {
"É": "Ī",
"à": "ṁ",
"ù": "ḥ",
"è": "ṝ", # long vocalic r (U+1E5D)
"û": "ṁl", # anusvara + l (ScaGoudy ligature glyph)
"ÿ": "ḷ", # vocalic l (U+1E37)
}
+428
View File
@@ -0,0 +1,428 @@
/**
* seed-db.ts — Push all extracted JSON data into MongoDB using the native driver.
*
* Prisma with MongoDB requires a replica set for writes (even createMany/deleteMany).
* This script uses the MongoDB native driver directly to avoid that constraint.
* The collections and field names match the Prisma schema so the frontend can
* still read via Prisma without issues.
*
* Usage:
* npx tsx scripts/seed-db.ts
*
* Prerequisites:
* - DATABASE_URL must be set in .env
* - All extraction scripts (bg, sb, cc, glossary) must have been run
*/
import dotenv from "dotenv";
import fs from "fs";
import path from "path";
import { MongoClient } from "mongodb";
// Load env
dotenv.config({ path: ".env.local" });
dotenv.config({ path: ".env" });
const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
console.error("❌ DATABASE_URL is not set in .env");
process.exit(1);
}
const BASE_DIR = path.resolve(__dirname, "..");
const LIB_DIR = path.join(BASE_DIR, "app", "lib");
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function readJson<T>(filePath: string): T {
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}
function readAllJsonInDir(dir: string): unknown[] {
if (!fs.existsSync(dir)) return [];
return fs
.readdirSync(dir)
.filter((f) => f.endsWith(".json"))
.sort()
.map((f) => readJson(path.join(dir, f)));
}
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type BgVerse = {
book_title: string;
chapter: number;
verse: number;
verse_end?: number;
reference: string;
devanagari: string;
transliteration: string;
synonyms: { word: string; meanings: string[] }[];
translation: string;
purport: string[];
};
type SbVerse = {
book_title: string;
canto: number;
chapter: number;
verse: number;
verse_end?: number;
reference: string;
devanagari: string;
transliteration: string;
synonyms: { word: string; meanings: string[] }[];
translation: string;
purport: string[];
};
type CcVerse = {
book_title: string;
lila: string;
chapter: number;
verse: number;
verse_end?: number;
reference: string;
devanagari: string;
bengali: string;
transliteration: string;
synonyms: { word: string; meanings: string[] }[];
translation: string;
purport: string[];
};
type GlossaryData = {
title: string;
description: string;
totalEntries: number;
letters: string[];
entries: {
term: string;
definition: string;
definition_html: string;
}[];
};
// ---------------------------------------------------------------------------
// BG
// ---------------------------------------------------------------------------
async function seedBg(db: ReturnType<MongoClient["db"]>) {
const introPath = path.join(LIB_DIR, "books", "bg", "intro.json");
if (!fs.existsSync(introPath)) {
console.log(" ⏭ BG intro.json not found, skipping");
return;
}
const intro = readJson(introPath);
// Book meta
const metaCol = db.collection("book_meta");
await metaCol.deleteMany({ book: "bg" });
await metaCol.insertOne({ book: "bg", data: intro });
console.log(" 📖 BG book meta inserted");
// Verses
const versesCol = db.collection("verses");
await versesCol.deleteMany({ book: "bg" });
const chaptersDir = path.join(LIB_DIR, "books", "bg", "chapters");
if (!fs.existsSync(chaptersDir)) {
console.log(" ⏭ BG 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<string, unknown>[] = [];
for (const chDir of chapterDirs) {
const versesDir = path.join(chaptersDir, chDir, "verses");
const verses = readAllJsonInDir(versesDir) as BgVerse[];
for (const v of verses) {
docs.push({
book: "bg",
division: null,
chapter: v.chapter,
verse: v.verse,
verseEnd: v.verse_end ?? null,
reference: v.reference,
bookTitle: v.book_title,
devanagari: v.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(` ✅ BG: ${docs.length} verses`);
}
// ---------------------------------------------------------------------------
// SB
// ---------------------------------------------------------------------------
async function seedSb(db: ReturnType<MongoClient["db"]>) {
const introPath = path.join(LIB_DIR, "books", "sb", "intro.json");
if (!fs.existsSync(introPath)) {
console.log(" ⏭ SB intro.json not found, skipping");
return;
}
const intro = readJson(introPath);
const metaCol = db.collection("book_meta");
await metaCol.deleteMany({ book: "sb" });
await metaCol.insertOne({ book: "sb", data: intro });
console.log(" 📖 SB book meta inserted");
const versesCol = db.collection("verses");
await versesCol.deleteMany({ book: "sb" });
const cantosDir = path.join(LIB_DIR, "books", "sb", "cantos");
if (!fs.existsSync(cantosDir)) {
console.log(" ⏭ SB cantos dir not found, skipping verses");
return;
}
const cantoDirs = fs
.readdirSync(cantosDir)
.filter((d) => fs.statSync(path.join(cantosDir, d)).isDirectory())
.sort();
let totalCount = 0;
for (const cantoDir of cantoDirs) {
const chaptersDir = path.join(cantosDir, cantoDir, "chapters");
if (!fs.existsSync(chaptersDir)) continue;
const chapterDirs = fs
.readdirSync(chaptersDir)
.filter((d) => fs.statSync(path.join(chaptersDir, d)).isDirectory())
.sort();
const docs: Record<string, unknown>[] = [];
for (const chDir of chapterDirs) {
const versesDir = path.join(chaptersDir, chDir, "verses");
const verses = readAllJsonInDir(versesDir) as SbVerse[];
for (const v of verses) {
docs.push({
book: "sb",
division: String(v.canto),
chapter: v.chapter,
verse: v.verse,
verseEnd: v.verse_end ?? null,
reference: v.reference,
bookTitle: v.book_title,
devanagari: v.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 });
}
totalCount += docs.length;
console.log(` Canto ${cantoDir}: ${docs.length} verses`);
}
console.log(` ✅ SB: ${totalCount} verses`);
}
// ---------------------------------------------------------------------------
// CC
// ---------------------------------------------------------------------------
async function seedCc(db: ReturnType<MongoClient["db"]>) {
const introPath = path.join(LIB_DIR, "books", "cc", "intro.json");
if (!fs.existsSync(introPath)) {
console.log(" ⏭ CC intro.json not found, skipping");
return;
}
const intro = readJson(introPath);
const metaCol = db.collection("book_meta");
await metaCol.deleteMany({ book: "cc" });
await metaCol.insertOne({ book: "cc", data: intro });
console.log(" 📖 CC book meta inserted");
const versesCol = db.collection("verses");
await versesCol.deleteMany({ book: "cc" });
const lilasDir = path.join(LIB_DIR, "books", "cc", "lilas");
if (!fs.existsSync(lilasDir)) {
console.log(" ⏭ CC lilas dir not found, skipping verses");
return;
}
const lilaDirs = fs
.readdirSync(lilasDir)
.filter((d) => fs.statSync(path.join(lilasDir, d)).isDirectory())
.sort();
let totalCount = 0;
for (const lilaDir of lilaDirs) {
const chaptersDir = path.join(lilasDir, lilaDir, "chapters");
if (!fs.existsSync(chaptersDir)) continue;
const chapterDirs = fs
.readdirSync(chaptersDir)
.filter((d) => fs.statSync(path.join(chaptersDir, d)).isDirectory())
.sort();
const docs: Record<string, unknown>[] = [];
for (const chDir of chapterDirs) {
const versesDir = path.join(chaptersDir, chDir, "verses");
const verses = readAllJsonInDir(versesDir) as CcVerse[];
for (const v of verses) {
docs.push({
book: "cc",
division: v.lila,
chapter: v.chapter,
verse: v.verse,
verseEnd: v.verse_end ?? null,
reference: v.reference,
bookTitle: v.book_title,
devanagari: v.devanagari,
bengali: v.bengali,
transliteration: v.transliteration,
synonyms: v.synonyms,
translation: v.translation,
purport: v.purport,
});
}
}
if (docs.length > 0) {
await versesCol.insertMany(docs, { ordered: false });
}
totalCount += docs.length;
console.log(` Līlā ${lilaDir}: ${docs.length} verses`);
}
console.log(` ✅ CC: ${totalCount} verses`);
}
// ---------------------------------------------------------------------------
// Glossary
// ---------------------------------------------------------------------------
async function seedGlossary(db: ReturnType<MongoClient["db"]>) {
const glossaryPath = path.join(LIB_DIR, "glossary", "index.json");
if (!fs.existsSync(glossaryPath)) {
console.log(" ⏭ Glossary index.json not found, skipping");
return;
}
const data = readJson<GlossaryData>(glossaryPath);
const col = db.collection("glossary");
await col.deleteMany({});
const docs = data.entries.map((entry) => ({
term: entry.term,
definition: entry.definition,
definitionHtml: entry.definition_html,
}));
if (docs.length > 0) {
await col.insertMany(docs, { ordered: false });
}
console.log(` ✅ Glossary: ${docs.length} entries`);
}
// ---------------------------------------------------------------------------
// Create indexes (matching Prisma schema)
// ---------------------------------------------------------------------------
async function createIndexes(db: ReturnType<MongoClient["db"]>) {
console.log(" 📇 Creating indexes...");
const versesCol = db.collection("verses");
await versesCol.createIndex({ book: 1 });
await versesCol.createIndex({ book: 1, division: 1 });
await versesCol.createIndex({ book: 1, division: 1, chapter: 1 });
await versesCol.createIndex(
{ book: 1, division: 1, chapter: 1, verse: 1 },
{ unique: true },
);
const metaCol = db.collection("book_meta");
await metaCol.createIndex({ book: 1 }, { unique: true });
const glossaryCol = db.collection("glossary");
await glossaryCol.createIndex({ term: 1 }, { unique: true });
console.log(" ✅ Indexes created");
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
console.log("🌱 Seeding database...\n");
const client = new MongoClient(DATABASE_URL!);
await client.connect();
console.log("📡 Connected to MongoDB\n");
const db = client.db(); // uses the database name from the URL
const start = Date.now();
console.log("[1/5] Bhagavad-gītā");
await seedBg(db);
console.log();
console.log("[2/5] Śrīmad-Bhāgavatam");
await seedSb(db);
console.log();
console.log("[3/5] Śrī Caitanya-caritāmṛta");
await seedCc(db);
console.log();
console.log("[4/5] Glossary");
await seedGlossary(db);
console.log();
console.log("[5/5] Indexes");
await createIndexes(db);
console.log();
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
console.log(`🎉 Done in ${elapsed}s`);
await client.close();
}
main().catch((e) => {
console.error("❌ Seed failed:", e);
process.exit(1);
});