OPEN SOURCE
software/scripts/download_sources.py
Read on this site and download the files you need.
Nichijoyaku was created using AI. A complete review by human biblical-language and biblical-studies specialists has not been established. Read alongside established translations and original-language resources.
← Back to the open-source guide
1"""Download and parse public domain Bible source texts."""23from __future__ import annotations45import json6import xml.etree.ElementTree as ET7from pathlib import Path89import httpx10from rich.console import Console11from rich.progress import (12BarColumn,13MofNCompleteColumn,14Progress,15SpinnerColumn,16TaskProgressColumn,17TextColumn,18TimeElapsedColumn,19)20from rich.table import Table2122# Base URLs for source data23MORPHGNT_BASE = "https://raw.githubusercontent.com/morphgnt/sblgnt/master"24MORPHHB_BASE = "https://raw.githubusercontent.com/openscriptures/morphhb/master/wlc"25OPENBIBLE_BASE = "https://raw.githubusercontent.com/churchstudio-org/openbible/main"2627console = Console()2829# NT Book mappings30# morphgnt uses XX-Abbr format, openbible uses name or name# format31NT_BOOKS = {32"matthew": {"morphgnt": "61-Mt", "openbible": "matthew"},33"mark": {"morphgnt": "62-Mk", "openbible": "mark"},34"luke": {"morphgnt": "63-Lk", "openbible": "luke"},35"john": {"morphgnt": "64-Jn", "openbible": "john"},36"acts": {"morphgnt": "65-Ac", "openbible": "acts"},37"romans": {"morphgnt": "66-Ro", "openbible": "romans"},38"1corinthians": {"morphgnt": "67-1Co", "openbible": "corinthians1"},39"2corinthians": {"morphgnt": "68-2Co", "openbible": "corinthians2"},40"galatians": {"morphgnt": "69-Ga", "openbible": "galatians"},41"ephesians": {"morphgnt": "70-Eph", "openbible": "ephesians"},42"philippians": {"morphgnt": "71-Php", "openbible": "philippians"},43"colossians": {"morphgnt": "72-Col", "openbible": "colossians"},44"1thessalonians": {"morphgnt": "73-1Th", "openbible": "thessalonians1"},45"2thessalonians": {"morphgnt": "74-2Th", "openbible": "thessalonians2"},46"1timothy": {"morphgnt": "75-1Ti", "openbible": "timothy1"},47"2timothy": {"morphgnt": "76-2Ti", "openbible": "timothy2"},48"titus": {"morphgnt": "77-Tit", "openbible": "titus"},49"philemon": {"morphgnt": "78-Phm", "openbible": "philemon"},50"hebrews": {"morphgnt": "79-Heb", "openbible": "hebrews"},51"james": {"morphgnt": "80-Jas", "openbible": "james"},52"1peter": {"morphgnt": "81-1Pe", "openbible": "peter1"},53"2peter": {"morphgnt": "82-2Pe", "openbible": "peter2"},54"1john": {"morphgnt": "83-1Jn", "openbible": "john1"},55"2john": {"morphgnt": "84-2Jn", "openbible": "john2"},56"3john": {"morphgnt": "85-3Jn", "openbible": "john3"},57"jude": {"morphgnt": "86-Jud", "openbible": "jude"},58"revelation": {"morphgnt": "87-Re", "openbible": "revelation"},59}6061# OT Book mappings62# morphhb uses book abbreviations, openbible uses full names63OT_BOOKS = {64"genesis": {"morphhb": "Gen", "openbible": "genesis"},65"exodus": {"morphhb": "Exod", "openbible": "exodus"},66"leviticus": {"morphhb": "Lev", "openbible": "leviticus"},67"numbers": {"morphhb": "Num", "openbible": "numbers"},68"deuteronomy": {"morphhb": "Deut", "openbible": "deuteronomy"},69"joshua": {"morphhb": "Josh", "openbible": "joshua"},70"judges": {"morphhb": "Judg", "openbible": "judges"},71"ruth": {"morphhb": "Ruth", "openbible": "ruth"},72"1samuel": {"morphhb": "1Sam", "openbible": "samuel1"},73"2samuel": {"morphhb": "2Sam", "openbible": "samuel2"},74"1kings": {"morphhb": "1Kgs", "openbible": "kings1"},75"2kings": {"morphhb": "2Kgs", "openbible": "kings2"},76"1chronicles": {"morphhb": "1Chr", "openbible": "chronicles1"},77"2chronicles": {"morphhb": "2Chr", "openbible": "chronicles2"},78"ezra": {"morphhb": "Ezra", "openbible": "ezra"},79"nehemiah": {"morphhb": "Neh", "openbible": "nehemiah"},80"esther": {"morphhb": "Esth", "openbible": "esther"},81"job": {"morphhb": "Job", "openbible": "job"},82"psalms": {"morphhb": "Ps", "openbible": "psalms"},83"proverbs": {"morphhb": "Prov", "openbible": "proverbs"},84"ecclesiastes": {"morphhb": "Eccl", "openbible": "ecclesiastes"},85"song_of_solomon": {"morphhb": "Song", "openbible": "songofsolomon"},86"isaiah": {"morphhb": "Isa", "openbible": "isaiah"},87"jeremiah": {"morphhb": "Jer", "openbible": "jeremiah"},88"lamentations": {"morphhb": "Lam", "openbible": "lamentations"},89"ezekiel": {"morphhb": "Ezek", "openbible": "ezekiel"},90"daniel": {"morphhb": "Dan", "openbible": "daniel"},91"hosea": {"morphhb": "Hos", "openbible": "hosea"},92"joel": {"morphhb": "Joel", "openbible": "joel"},93"amos": {"morphhb": "Amos", "openbible": "amos"},94"obadiah": {"morphhb": "Obad", "openbible": "obadiah"},95"jonah": {"morphhb": "Jonah", "openbible": "jonah"},96"micah": {"morphhb": "Mic", "openbible": "micah"},97"nahum": {"morphhb": "Nah", "openbible": "nahum"},98"habakkuk": {"morphhb": "Hab", "openbible": "habakkuk"},99"zephaniah": {"morphhb": "Zeph", "openbible": "zephaniah"},100"haggai": {"morphhb": "Hag", "openbible": "haggai"},101"zechariah": {"morphhb": "Zech", "openbible": "zechariah"},102"malachi": {"morphhb": "Mal", "openbible": "malachi"},103}104105# Combined mapping for convenience106ALL_BOOKS = {**OT_BOOKS, **NT_BOOKS}107108109def download_file(url: str) -> str | None:110"""Download a file from URL."""111try:112response = httpx.get(url, timeout=30.0, follow_redirects=True)113response.raise_for_status()114return response.text115except httpx.HTTPError:116return None117118119def parse_morphgnt_book(content: str) -> dict[str, dict]:120"""Parse MorphGNT space-separated data into verse dictionary.121122Format: BBCCVV pos parsing text_punct word norm lemma (space-separated)123where BB=book, CC=chapter, VV=verse124"""125verses: dict[str, dict] = {}126127for line in content.strip().split("\n"):128if not line.strip():129continue130131parts = line.split()132if len(parts) < 7:133continue134135# Parse reference (format: BBCCVV where BB=book, CC=chapter, VV=verse)136ref = parts[0]137try:138chapter = int(ref[2:4])139verse = int(ref[4:6])140except (ValueError, IndexError):141continue142143ref_key = f"{chapter}:{verse}"144145if ref_key not in verses:146verses[ref_key] = {"words": [], "text": ""}147148# Add word data149# Format: ref pos parsing text_punct word norm lemma150word_data = {151"word": parts[4], # Word without punctuation152"normalized": parts[5],153"lemma": parts[6],154"pos": parts[1],155"parsing": parts[2],156}157verses[ref_key]["words"].append(word_data)158159# Build full text for each verse160for ref_key, data in verses.items():161words = [w["word"] for w in data["words"]]162data["text"] = " ".join(words)163164# Convert to simple format: ref -> {text, translit}165result = {}166for ref_key, data in verses.items():167result[ref_key] = {168"text": data["text"],169"words": data["words"],170}171172return result173174175def parse_morphhb_xml(content: str) -> dict[str, dict]:176"""Parse MorphHB OSIS XML format into verse dictionary.177178MorphHB uses OSIS XML format with <verse> elements containing <w> word elements.179"""180verses: dict[str, dict] = {}181182try:183# Parse XML184root = ET.fromstring(content)185except ET.ParseError:186return {}187188# Find all verse elements189for verse_elem in root.iter(190"{http://www.bibletechnologies.net/2003/OSIS/namespace}verse"191):192osisID = verse_elem.get("osisID", "")193if not osisID:194continue195196# Parse osisID format: Book.Chapter.Verse197parts = osisID.split(".")198if len(parts) < 3:199continue200201try:202chapter = int(parts[1])203verse = int(parts[2])204except ValueError:205continue206207ref_key = f"{chapter}:{verse}"208209# Collect words from this verse210words = []211word_data_list = []212213for w_elem in verse_elem.iter(214"{http://www.bibletechnologies.net/2003/OSIS/namespace}w"215):216word = w_elem.text or ""217word = word.strip()218if not word:219continue220221lemma = w_elem.get("lemma", "")222morph = w_elem.get("morph", "")223224words.append(word)225word_data_list.append({226"word": word,227"lemma": lemma,228"morph": morph,229})230231if words:232verses[ref_key] = {233"text": " ".join(words),234"words": word_data_list,235}236237return verses238239240def parse_openbible_json(content: str) -> dict[str, str]:241"""Parse OpenBible JSON format into verse dictionary.242243OpenBible uses 3D array: bible[book][chapter][verse]244But individual book files use: book[chapter][verse]245"""246try:247data = json.loads(content)248except json.JSONDecodeError:249return {}250251verses = {}252253# Handle both full bible and individual book formats254if isinstance(data, list):255# Individual book: [chapter][verse]256for chapter_idx, chapter in enumerate(data):257if not isinstance(chapter, list):258continue259for verse_idx, verse_text in enumerate(chapter):260if verse_text:261ref_key = f"{chapter_idx + 1}:{verse_idx + 1}"262verses[ref_key] = verse_text263264return verses265266267def download_greek_nt(output_dir: Path, book: str) -> tuple[bool, int]:268"""Download Greek NT from MorphGNT.269270Returns:271Tuple of (success, verse_count)272"""273if book not in NT_BOOKS:274return False, 0275276morphgnt_code = NT_BOOKS[book]["morphgnt"]277url = f"{MORPHGNT_BASE}/{morphgnt_code}-morphgnt.txt"278279content = download_file(url)280if not content:281return False, 0282283verses = parse_morphgnt_book(content)284285# Save to JSON286output_file = output_dir / "greek" / f"{book}.json"287output_file.parent.mkdir(parents=True, exist_ok=True)288289with output_file.open("w", encoding="utf-8") as f:290json.dump(verses, f, ensure_ascii=False, indent=2)291292return True, len(verses)293294295def download_hebrew_ot(output_dir: Path, book: str) -> tuple[bool, int]:296"""Download Hebrew OT from MorphHB (Westminster Leningrad Codex).297298Returns:299Tuple of (success, verse_count)300"""301if book not in OT_BOOKS:302return False, 0303304morphhb_code = OT_BOOKS[book]["morphhb"]305url = f"{MORPHHB_BASE}/{morphhb_code}.xml"306307content = download_file(url)308if not content:309return False, 0310311verses = parse_morphhb_xml(content)312313# Save to JSON314output_file = output_dir / "hebrew" / f"{book}.json"315output_file.parent.mkdir(parents=True, exist_ok=True)316317with output_file.open("w", encoding="utf-8") as f:318json.dump(verses, f, ensure_ascii=False, indent=2)319320return True, len(verses)321322323def download_kjv(output_dir: Path, book: str) -> tuple[bool, int]:324"""Download KJV from OpenBible.325326Returns:327Tuple of (success, verse_count)328"""329if book in NT_BOOKS:330openbible_name = NT_BOOKS[book]["openbible"]331elif book in OT_BOOKS:332openbible_name = OT_BOOKS[book]["openbible"]333else:334return False, 0335336url = f"{OPENBIBLE_BASE}/KJV/books/{openbible_name}.json"337338content = download_file(url)339if not content:340return False, 0341342verses = parse_openbible_json(content)343344# Save to JSON345output_file = output_dir / "english" / "kjv" / f"{book}.json"346output_file.parent.mkdir(parents=True, exist_ok=True)347348with output_file.open("w", encoding="utf-8") as f:349json.dump(verses, f, ensure_ascii=False, indent=2)350351return True, len(verses)352353354def download_kougo(output_dir: Path, book: str) -> tuple[bool, int]:355"""Download Japanese Kougo-yaku from OpenBible.356357Returns:358Tuple of (success, verse_count)359"""360if book in NT_BOOKS:361openbible_name = NT_BOOKS[book]["openbible"]362elif book in OT_BOOKS:363openbible_name = OT_BOOKS[book]["openbible"]364else:365return False, 0366367url = f"{OPENBIBLE_BASE}/KOUGO_YAKU/books/{openbible_name}.json"368369content = download_file(url)370if not content:371return False, 0372373verses = parse_openbible_json(content)374375# Save to Japanese kougo directory376output_file = output_dir / "japanese" / "kougo" / f"{book}.json"377output_file.parent.mkdir(parents=True, exist_ok=True)378379with output_file.open("w", encoding="utf-8") as f:380json.dump(verses, f, ensure_ascii=False, indent=2)381382return True, len(verses)383384385def download_book_with_progress(386output_dir: Path,387book: str,388progress: Progress,389book_task: int,390) -> dict[str, tuple[bool, int]]:391"""Download all sources for a book with progress updates.392393Returns:394Dictionary of source type to (success, verse_count)395"""396is_ot = book in OT_BOOKS397results: dict[str, tuple[bool, int]] = {}398399# Create sub-tasks for each source type400if is_ot:401sources = [402("Hebrew", lambda: download_hebrew_ot(output_dir, book)),403("KJV", lambda: download_kjv(output_dir, book)),404("Kougo", lambda: download_kougo(output_dir, book)),405]406else:407sources = [408("Greek", lambda: download_greek_nt(output_dir, book)),409("KJV", lambda: download_kjv(output_dir, book)),410("Kougo", lambda: download_kougo(output_dir, book)),411]412413for source_name, download_func in sources:414progress.update(book_task, description=f"[cyan]{book}[/] - {source_name}")415success, count = download_func()416results[source_name.lower()] = (success, count)417progress.advance(book_task)418419return results420421422def create_progress() -> Progress:423"""Create a rich Progress instance with custom columns."""424return Progress(425SpinnerColumn(),426TextColumn("[bold blue]{task.description}"),427BarColumn(bar_width=40),428TaskProgressColumn(),429MofNCompleteColumn(),430TimeElapsedColumn(),431console=console,432expand=False,433)434435436def download_all_books(437output_dir: Path,438books: dict[str, dict],439testament_name: str,440) -> dict[str, dict[str, tuple[bool, int]]]:441"""Download all books with visual progress bars.442443Args:444output_dir: Output directory for source files.445books: Dictionary of book names to their metadata.446testament_name: "Old Testament" or "New Testament" for display.447448Returns:449Dictionary of book name to source results.450"""451all_results: dict[str, dict[str, tuple[bool, int]]] = {}452453console.print(f"\n[bold green]{testament_name}[/] ({len(books)} books)")454console.print("─" * 60)455456with create_progress() as progress:457# Overall progress for all books458overall_task = progress.add_task(459f"[yellow]Downloading {testament_name}...",460total=len(books),461)462463for book in books:464# Per-book progress (3 sources each)465book_task = progress.add_task(466f"[cyan]{book}[/]",467total=3,468)469470results = download_book_with_progress(471output_dir, book, progress, book_task472)473all_results[book] = results474475# Mark book task as complete and update overall476progress.update(book_task, visible=False)477progress.advance(overall_task)478479return all_results480481482def print_summary(483ot_results: dict[str, dict[str, tuple[bool, int]]] | None,484nt_results: dict[str, dict[str, tuple[bool, int]]] | None,485) -> None:486"""Print a summary table of download results."""487console.print("\n[bold]Download Summary[/]")488console.print("=" * 60)489490table = Table(show_header=True, header_style="bold magenta")491table.add_column("Testament", style="cyan")492table.add_column("Books", justify="right")493table.add_column("Source", justify="right")494table.add_column("KJV", justify="right")495table.add_column("Kougo", justify="right")496table.add_column("Total Verses", justify="right")497498if ot_results:499ot_source_ok = sum(1 for r in ot_results.values() if r.get("hebrew", (False, 0))[0])500ot_kjv_ok = sum(1 for r in ot_results.values() if r.get("kjv", (False, 0))[0])501ot_kougo_ok = sum(1 for r in ot_results.values() if r.get("kougo", (False, 0))[0])502ot_verses = sum(r.get("hebrew", (False, 0))[1] for r in ot_results.values())503table.add_row(504"Old Testament",505str(len(ot_results)),506f"[green]{ot_source_ok}[/] Hebrew",507f"[green]{ot_kjv_ok}[/]",508f"[green]{ot_kougo_ok}[/]",509f"{ot_verses:,}",510)511512if nt_results:513nt_source_ok = sum(1 for r in nt_results.values() if r.get("greek", (False, 0))[0])514nt_kjv_ok = sum(1 for r in nt_results.values() if r.get("kjv", (False, 0))[0])515nt_kougo_ok = sum(1 for r in nt_results.values() if r.get("kougo", (False, 0))[0])516nt_verses = sum(r.get("greek", (False, 0))[1] for r in nt_results.values())517table.add_row(518"New Testament",519str(len(nt_results)),520f"[green]{nt_source_ok}[/] Greek",521f"[green]{nt_kjv_ok}[/]",522f"[green]{nt_kougo_ok}[/]",523f"{nt_verses:,}",524)525526console.print(table)527528# Total529total_books = (len(ot_results) if ot_results else 0) + (len(nt_results) if nt_results else 0)530total_verses = (531(sum(r.get("hebrew", (False, 0))[1] for r in ot_results.values()) if ot_results else 0) +532(sum(r.get("greek", (False, 0))[1] for r in nt_results.values()) if nt_results else 0)533)534console.print(f"\n[bold green]Total: {total_books} books, {total_verses:,} verses[/]")535536537def download_single_book(output_dir: Path, book: str) -> None:538"""Download a single book with progress display."""539if book in OT_BOOKS:540testament = "Old Testament"541books = {book: OT_BOOKS[book]}542elif book in NT_BOOKS:543testament = "New Testament"544books = {book: NT_BOOKS[book]}545else:546console.print(f"[red]Unknown book: {book}[/]")547return548549results = download_all_books(output_dir, books, testament)550551# Show results for single book552book_results = results.get(book, {})553console.print(f"\n[bold]{book.title()}[/] download complete:")554for source, (success, count) in book_results.items():555status = "[green]OK[/]" if success else "[red]FAILED[/]"556console.print(f" {source}: {status} ({count} verses)")557558559def main() -> None:560"""Main entry point."""561import argparse562563parser = argparse.ArgumentParser(564description="Download Bible source texts with progress display"565)566parser.add_argument(567"--book",568default=None,569help="Single book to download",570)571parser.add_argument(572"--output",573default="data/sources",574help="Output directory (default: data/sources)",575)576parser.add_argument(577"--all-nt",578action="store_true",579help="Download all NT books",580)581parser.add_argument(582"--all-ot",583action="store_true",584help="Download all OT books",585)586parser.add_argument(587"--all",588action="store_true",589help="Download all books (OT + NT)",590)591592args = parser.parse_args()593output_dir = Path(args.output)594595console.print("[bold]Bible Source Downloader[/]")596console.print(f"Output directory: {output_dir}")597598ot_results = None599nt_results = None600601if args.all:602ot_results = download_all_books(output_dir, OT_BOOKS, "Old Testament")603nt_results = download_all_books(output_dir, NT_BOOKS, "New Testament")604print_summary(ot_results, nt_results)605elif args.all_ot:606ot_results = download_all_books(output_dir, OT_BOOKS, "Old Testament")607print_summary(ot_results, None)608elif args.all_nt:609nt_results = download_all_books(output_dir, NT_BOOKS, "New Testament")610print_summary(None, nt_results)611elif args.book:612download_single_book(output_dir, args.book)613else:614# Default: download john615download_single_book(output_dir, "john")616617618if __name__ == "__main__":619main()620