Pre-publication review draft
日常訳NICHIJOYAKU日本語 ↗

OPEN SOURCE

software/main.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

software/main.py1395 linesDownload this file ↓

This is the same source included in the download. Each line number is a link you can save or share.

File verification detailsSHA-256 255ba82d03a946faec73cc225a944c6e86605bfba9f0fd2d5c0d0e3fb05fd1af
1"""Japanese Bible Translator - Main entry point."""23from __future__ import annotations45import argparse6import json7import sys8from pathlib import Path9from typing import TYPE_CHECKING1011if TYPE_CHECKING:12    from src.orchestration import JobOrchestrator13    from src.orchestration.models import JobResult1415from rich.console import Console16from rich.live import Live17from rich.progress import (18    BarColumn,19    MofNCompleteColumn,20    Progress,21    SpinnerColumn,22    TaskProgressColumn,23    TextColumn,24    TimeElapsedColumn,25)26from rich.table import Table2728from src.config.style_config import StyleConfigManager29from src.context.builder import ContextBuilder30from src.context.glossary import GlossaryManager31from src.context.pericope import PericopeManager32from src.context.translation_memory import TranslationMemoryManager33from src.data.loader import DataLoader34from src.data.storage import Storage35from src.evaluation.evaluator import TranslationEvaluator36from src.evaluation.storage import EvaluationStorage37from src.models.pericope import Pericope38from src.models.style import TranslationStyle39from src.models.verse import VerseReference40from src.output.formatters import OutputFormatter41from src.pipeline.prompt import PromptBuilder42from src.pipeline.translator import DEFAULT_MODEL, Translator43from src.validation.checks import TranslationValidator4445console = Console()464748def tui_cmd(args: argparse.Namespace) -> int:49    """Handle the tui command."""50    from src.tui.app import run_tui5152    data_dir = Path(args.data_dir) if args.data_dir else get_data_dir()53    run_tui(data_dir)54    return 0555657def get_data_dir() -> Path:58    """Get the data directory path."""59    return Path(__file__).parent / "data"606162def create_translator(63    auto_evaluate: bool = True,64    use_tm: bool = True,65) -> Translator:66    """Create and configure the translator.6768    Args:69        auto_evaluate: Whether to enable auto-evaluation (default: True).70        use_tm: Whether to enable translation memory.7172    Returns:73        Configured Translator instance.74    """75    data_dir = get_data_dir()76    loader = DataLoader(data_dir)77    storage = Storage(data_dir)7879    # Load glossary80    glossary_terms = loader.load_glossary()81    glossary = GlossaryManager(glossary_terms)8283    # Load style configuration84    style_manager = StyleConfigManager(data_dir / "config" / "style.json")85    style = style_manager.get_style()8687    # Create TM if enabled88    tm = None89    if use_tm:90        tm = TranslationMemoryManager(data_dir / "memory" / "segments.json")9192    # Create pericope manager93    pericope_manager = PericopeManager(data_dir / "metadata" / "pericopes.json")9495    # Create components96    context_builder = ContextBuilder(loader, glossary, pericope_manager=pericope_manager)97    prompt_builder = PromptBuilder(glossary, style=style, translation_memory=tm)9899    # Always create evaluator and storage (auto_evaluate defaults to True)100    evaluator = TranslationEvaluator(data_dir=data_dir)101    eval_storage = EvaluationStorage(data_dir)102103    return Translator(104        context_builder,105        prompt_builder,106        storage,107        translation_memory=tm,108        auto_evaluate=auto_evaluate,109        evaluator=evaluator,110        evaluation_storage=eval_storage,111    )112113114def translate_verse_cmd(args: argparse.Namespace) -> int:115    """Handle the verse command."""116    try:117        ref = VerseReference.from_string(args.verse)118    except ValueError as e:119        console.print(f"[red]Error:[/red] {e}")120        return 1121122    console.print(f"Translating {ref}...")123124    try:125        # Evaluations are automatic by default, --no-evaluate disables126        auto_eval = not getattr(args, 'no_evaluate', False)127        translator = create_translator(auto_evaluate=auto_eval)128        output = translator.translate_verse(ref, skip_existing=not args.force)129130        console.print(f"\n[green]Translation:[/green]")131        console.print(f"  {output.text}")132        console.print(f"\n[dim]Model: {output.model}[/dim]")133134        # Validate135        if args.validate:136            validator = TranslationValidator()137            report = validator.validate(output)138            if not report.all_passed:139                console.print("\n[yellow]Validation warnings:[/yellow]")140                for result in report.failed_checks:141                    console.print(f"  - {result.message}")142143        if auto_eval:144            console.print("[dim]Evaluation saved[/dim]")145146    except ValueError as e:147        console.print(f"[red]Error:[/red] {e}")148        return 1149150    return 0151152153def translate_chapter_cmd(args: argparse.Namespace) -> int:154    """Handle the chapter command."""155    book = args.book156    chapter = args.chapter157158    console.print(f"Translating {book.title()} chapter {chapter}...")159160    try:161        # Evaluations are automatic by default, --no-evaluate disables162        auto_eval = not getattr(args, 'no_evaluate', False)163        translator = create_translator(auto_evaluate=auto_eval)164165        def progress_callback(current: int, total: int, ref: VerseReference) -> None:166            console.print(f"  [{current}/{total}] {ref}")167168        outputs = translator.translate_chapter(169            book,170            chapter,171            skip_existing=not args.force,172            progress_callback=progress_callback,173        )174175        console.print(f"\n[green]Completed {len(outputs)} verses[/green]")176177        if auto_eval:178            console.print("[dim]Evaluations saved[/dim]")179180    except ValueError as e:181        console.print(f"[red]Error:[/red] {e}")182        return 1183184    return 0185186187def translate_book_cmd(args: argparse.Namespace) -> int:188    """Handle the book command."""189    book = args.book190191    console.print(f"Translating {book.title()}...")192193    try:194        # Evaluations are automatic by default, --no-evaluate disables195        auto_eval = not getattr(args, 'no_evaluate', False)196        translator = create_translator(auto_evaluate=auto_eval)197        loader = DataLoader(get_data_dir())198        chapters = loader.get_book_chapters(book)199200        if not chapters:201            console.print(f"[yellow]No chapters found for {book}[/yellow]")202            return 1203204        total_verses = 0205        with Progress(206            SpinnerColumn(),207            TextColumn("[progress.description]{task.description}"),208            console=console,209        ) as progress:210            for chapter in chapters:211                task = progress.add_task(f"Chapter {chapter}...", total=None)212                outputs = translator.translate_chapter(213                    book, chapter, skip_existing=not args.force214                )215                total_verses += len(outputs)216                progress.remove_task(task)217218        console.print(f"\n[green]Completed {total_verses} verses[/green]")219        if auto_eval:220            console.print("[dim]Evaluations saved[/dim]")221222    except ValueError as e:223        console.print(f"[red]Error:[/red] {e}")224        return 1225226    return 0227228229def export_cmd(args: argparse.Namespace) -> int:230    """Handle the export command."""231    data_dir = get_data_dir()232    storage = Storage(data_dir)233    formatter = OutputFormatter(storage)234    output_dir = Path(args.output) if args.output else data_dir / "exports"235236    book = args.book237    formats = args.formats238239    console.print(f"Exporting {book}...")240241    for fmt in formats:242        if fmt == "json":243            path = formatter.export_book_json(book, output_dir)244            console.print(f"  [green]JSON:[/green] {path}")245        elif fmt == "text":246            path = formatter.export_book_text(book, output_dir)247            console.print(f"  [green]Text:[/green] {path}")248        elif fmt == "html":249            # Export each chapter as HTML250            loader = DataLoader(data_dir)251            metadata = loader.load_book_metadata(book)252            chapters = loader.get_book_chapters(book)253            for chapter in chapters:254                path = formatter.export_chapter_html(255                    book, chapter, output_dir / "html", metadata256                )257            console.print(f"  [green]HTML:[/green] {output_dir / 'html'}")258259    return 0260261262def status_cmd(args: argparse.Namespace) -> int:263    """Handle the status command."""264    data_dir = get_data_dir()265    storage = Storage(data_dir)266    loader = DataLoader(data_dir)267268    book = args.book269270    # Get book metadata271    metadata = loader.load_book_metadata(book)272    total_verses = metadata.verse_count if metadata else 0273274    if total_verses == 0:275        # Estimate from source files276        chapters = loader.get_book_chapters(book)277        for chapter in chapters:278            verses = loader.load_chapter_verses(book, chapter)279            total_verses += len(verses)280281    stats = storage.get_completion_stats(book, total_verses)282283    table = Table(title=f"{book.title()} Translation Status")284    table.add_column("Metric", style="cyan")285    table.add_column("Value", style="green")286287    table.add_row("Completed verses", str(stats["completed"]))288    table.add_row("Total verses", str(stats["total"]))289    table.add_row("Progress", f"{stats['percentage']}%")290    table.add_row("Chapters started", str(stats["chapters_started"]))291292    console.print(table)293294    return 0295296297def evaluate_cmd(args: argparse.Namespace) -> int:298    """Handle the evaluate command."""299    from src.evaluation.evaluator import (300        EVALUATION_SYSTEM_PROMPT,301        TranslationEvaluator,302        parse_evaluation_response,303    )304305    data_dir = get_data_dir()306    loader = DataLoader(data_dir)307    storage = Storage(data_dir)308    eval_storage = EvaluationStorage(data_dir)309310    # Load glossary311    glossary_terms = loader.load_glossary()312    glossary = GlossaryManager(glossary_terms)313    context_builder = ContextBuilder(loader, glossary)314315    evaluator = TranslationEvaluator(data_dir=data_dir)316    book = args.book317    chapter = args.chapter318319    console.print(f"Evaluating {book.title()} chapter {chapter}...")320321    try:322        verses = loader.load_chapter_verses(book, chapter)323        verse_evaluations = {}324325        for i, verse in enumerate(verses):326            ref = verse.reference327            console.print(f"  [{i + 1}/{len(verses)}] {ref}")328329            # Load translation330            translation = storage.load_translation(ref)331            if not translation:332                console.print(f"    [yellow]No translation found, skipping[/yellow]")333                continue334335            # Build context and evaluation prompt336            context = context_builder.build_context(ref)337            eval_prompt = evaluator.build_verse_prompt(translation.text, context)338339            # Call Claude CLI for evaluation340            full_prompt = f"{EVALUATION_SYSTEM_PROMPT}\n\n{eval_prompt}"341            try:342                import subprocess343                result = subprocess.run(344                    ["claude", "--print", "-p", full_prompt, "--model", DEFAULT_MODEL],345                    capture_output=True,346                    text=True,347                    check=False,348                    timeout=120,349                )350                if result.returncode != 0:351                    raise RuntimeError(f"Claude CLI error: {result.stderr}")352                eval_result = result.stdout353            except Exception as e:354                console.print(f"    [red]Evaluation failed: {e}[/red]")355                continue356357            try:358                scores = parse_evaluation_response(eval_result.strip())359                evaluation = evaluator.create_verse_evaluation(360                    verse_number=ref.verse, scores=scores361                )362                verse_evaluations[ref.verse] = evaluation363            except ValueError as e:364                console.print(f"    [red]Parse error: {e}[/red]")365                continue366367        # Create and save chapter evaluation368        if verse_evaluations:369            chapter_eval = evaluator.create_chapter_evaluation(370                book, chapter, verse_evaluations371            )372            eval_storage.save_chapter_evaluation(chapter_eval)373            console.print(374                f"\n[green]Evaluated {len(verse_evaluations)} verses[/green]"375            )376            console.print(377                f"Average score: {chapter_eval.overall_score:.1f}"378            )379        else:380            console.print("[yellow]No verses evaluated[/yellow]")381382    except ValueError as e:383        console.print(f"[red]Error:[/red] {e}")384        return 1385386    return 0387388389def style_show_cmd(args: argparse.Namespace) -> int:390    """Handle the style show command."""391    data_dir = get_data_dir()392    manager = StyleConfigManager(data_dir / "config" / "style.json")393    style = manager.get_style()394395    table = Table(title="Translation Style Configuration")396    table.add_column("Setting", style="cyan")397    table.add_column("Value", style="green")398399    table.add_row("Equivalence Approach", style.equivalence_approach.value)400    table.add_row("Gender Language", style.gender_language.value)401    table.add_row("Readability Level", style.readability_level.value)402    table.add_row("Theological Tradition", style.theological_tradition.value)403    table.add_row("Textual Basis", style.textual_basis)404    table.add_row("Term Consistency", style.term_consistency.value)405    table.add_row("Formality Level", style.formality_level.value)406    table.add_row("Avoid Archaic", ", ".join(style.avoid_archaic))407    table.add_row("Target Style Reference", style.target_style_reference)408    if style.additional_instructions:409        table.add_row("Additional Instructions", style.additional_instructions)410411    console.print(table)412    console.print(f"\n[dim]Config file: {manager.config_path}[/dim]")413414    return 0415416417def style_edit_cmd(args: argparse.Namespace) -> int:418    """Handle the style edit command."""419    data_dir = get_data_dir()420    manager = StyleConfigManager(data_dir / "config" / "style.json")421    style = manager.get_style()422423    # Apply changes from arguments424    changed = False425426    if args.equivalence:427        from src.models.style import EquivalenceApproach428429        style.equivalence_approach = EquivalenceApproach(args.equivalence)430        changed = True431432    if args.gender:433        from src.models.style import GenderLanguage434435        style.gender_language = GenderLanguage(args.gender)436        changed = True437438    if args.readability:439        from src.models.style import ReadabilityLevel440441        style.readability_level = ReadabilityLevel(args.readability)442        changed = True443444    if args.tradition:445        from src.models.style import TheologicalTradition446447        style.theological_tradition = TheologicalTradition(args.tradition)448        changed = True449450    if args.consistency:451        from src.models.style import TermConsistency452453        style.term_consistency = TermConsistency(args.consistency)454        changed = True455456    if args.formality:457        from src.models.style import FormalityLevel458459        style.formality_level = FormalityLevel(args.formality)460        changed = True461462    if changed:463        manager.save(style)464        console.print("[green]Style configuration updated[/green]")465        return style_show_cmd(args)466    else:467        console.print("[yellow]No changes specified[/yellow]")468        console.print("Use --help to see available options")469        return 1470471472def tm_status_cmd(args: argparse.Namespace) -> int:473    """Handle the tm status command."""474    data_dir = get_data_dir()475    tm = TranslationMemoryManager(data_dir / "memory" / "segments.json")476    stats = tm.get_statistics()477478    table = Table(title="Translation Memory Status")479    table.add_column("Metric", style="cyan")480    table.add_column("Value", style="green")481482    table.add_row("Total Segments", str(stats.total_segments))483    table.add_row("Verse Segments", str(stats.verse_segments))484    table.add_row("Phrase Segments", str(stats.phrase_segments))485    table.add_row("Total Reuses", str(stats.total_reuses))486    table.add_row("Average Reuse", f"{stats.average_reuse:.2f}")487    table.add_row("Books Covered", ", ".join(stats.books_covered) or "None")488489    console.print(table)490491    return 0492493494def pericope_list_cmd(args: argparse.Namespace) -> int:495    """Handle the pericope list command."""496    data_dir = get_data_dir()497    pericope_manager = PericopeManager(data_dir / "metadata" / "pericopes.json")498499    book = args.book.lower()500501    if not pericope_manager.has_pericope_data(book):502        console.print(f"[yellow]No pericope data available for {book}[/yellow]")503        console.print("Run 'uv run python scripts/fetch_pericope_data.py --auto-generate' to generate")504        return 1505506    pericopes = pericope_manager.get_book_pericopes(book)507508    table = Table(title=f"Pericopes for {book.title()}")509    table.add_column("ID", style="cyan")510    table.add_column("Range", style="green")511    table.add_column("Verses", style="yellow")512    table.add_column("Title", style="white")513514    for p in pericopes:515        table.add_row(516            p.id,517            p.get_display_range(),518            str(p.verse_count),519            p.title or "[auto-generated]",520        )521522    console.print(table)523    console.print(f"\n[dim]Total: {len(pericopes)} pericopes[/dim]")524525    return 0526527528def pericope_translate_cmd(args: argparse.Namespace) -> int:529    """Handle the pericope translate command."""530    pericope_id = args.pericope_id531532    console.print(f"Translating pericope {pericope_id}...")533534    try:535        translator = create_translator()536        output = translator.translate_chunk_by_id(537            pericope_id, skip_existing=not args.force538        )539540        if output is None:541            console.print(f"[red]Pericope not found: {pericope_id}[/red]")542            return 1543544        console.print(f"\n[green]Translated {output.verse_count} verses:[/green]")545546        # Show translations547        for verse_ref, text in sorted(output.translations.items()):548            console.print(f"  [{verse_ref}] {text[:50]}..." if len(text) > 50 else f"  [{verse_ref}] {text}")549550        console.print(f"\n[dim]Model: {output.model}[/dim]")551        if output.coherence_note:552            console.print(f"[dim]Note: {output.coherence_note}[/dim]")553554    except ValueError as e:555        console.print(f"[red]Error:[/red] {e}")556        return 1557558    return 0559560561def pericope_chapter_cmd(args: argparse.Namespace) -> int:562    """Handle the pericope chapter command."""563    book = args.book.lower()564    chapter = args.chapter565566    console.print(f"Translating {book.title()} chapter {chapter} by pericopes...")567568    try:569        translator = create_translator()570571        def progress_callback(current: int, total: int, pericope: Pericope) -> None:572            title = pericope.title or pericope.get_display_range()573            console.print(f"  [{current}/{total}] {title}")574575        outputs = translator.translate_chapter_by_pericopes(576            book,577            chapter,578            skip_existing=not args.force,579            progress_callback=progress_callback,580        )581582        total_verses = sum(o.verse_count for o in outputs)583        console.print(f"\n[green]Completed {len(outputs)} pericopes ({total_verses} verses)[/green]")584585    except ValueError as e:586        console.print(f"[red]Error:[/red] {e}")587        return 1588589    return 0590591592def pericope_stats_cmd(args: argparse.Namespace) -> int:593    """Handle the pericope stats command."""594    data_dir = get_data_dir()595    pericope_manager = PericopeManager(data_dir / "metadata" / "pericopes.json")596597    stats = pericope_manager.get_statistics()598599    table = Table(title="Pericope Data Statistics")600    table.add_column("Metric", style="cyan")601    table.add_column("Value", style="green")602603    table.add_row("Books with Data", str(stats["books_with_data"]))604    table.add_row("Book Names", ", ".join(stats["book_names"]) or "None")605    table.add_row("Total Pericopes", str(stats["total_pericopes"]))606    table.add_row("Total Verses Covered", str(stats["total_verses_covered"]))607608    console.print(table)609610    return 0611612613def run_with_progress(614    orchestrator: "JobOrchestrator",615    job_id: str,616    books: list[str],617) -> "JobResult":618    """Run translation with live progress bars.619620    Args:621        orchestrator: JobOrchestrator instance.622        job_id: Job ID to run.623        books: List of books to translate.624625    Returns:626        JobResult from the orchestrator.627    """628    import threading629    import time630631    from src.orchestration.models import JobStatus632    from src.orchestration.state import JobStateStore633634    result_container: list = []635    error_container: list = []636637    def run_translation():638        try:639            result = orchestrator.run(job_id)640            result_container.append(result)641        except Exception as e:642            error_container.append(e)643644    # Start translation in background thread645    thread = threading.Thread(target=run_translation, daemon=True)646    thread.start()647648    # Create progress bars649    progress = Progress(650        SpinnerColumn(),651        TextColumn("[bold blue]{task.description}"),652        BarColumn(bar_width=40),653        TaskProgressColumn(),654        MofNCompleteColumn(),655        TimeElapsedColumn(),656        console=console,657        expand=False,658    )659660    # Track book progress tasks661    book_tasks: dict[str, int] = {}662    overall_task = None663664    with progress:665        # Overall progress bar666        overall_task = progress.add_task(667            "[yellow]Overall Progress",668            total=len(books),669        )670671        # Per-book progress bars (initially hidden until initialized)672        for book in books:673            book_tasks[book] = progress.add_task(674                f"[cyan]{book}",675                total=1,  # Will be updated when verse count is known676                visible=False,677            )678679        # Poll for progress updates680        state_store = JobStateStore(orchestrator.data_dir / "jobs" / "state.db")681682        while thread.is_alive():683            # Get current book statuses684            book_jobs = state_store.get_book_jobs(job_id)685686            books_completed = 0687            for bj in book_jobs:688                task_id = book_tasks.get(bj.book)689                if task_id is None:690                    continue691692                # Update verse count if known693                if bj.total_verses > 0:694                    progress.update(695                        task_id,696                        total=bj.total_verses,697                        completed=bj.completed_verses,698                        visible=True,699                    )700701                    # Update description with status702                    if bj.status == JobStatus.IN_PROGRESS:703                        desc = f"[cyan]{bj.book}[/] [dim]({bj.completed_verses}/{bj.total_verses})[/]"704                    elif bj.status == JobStatus.COMPLETED:705                        desc = f"[green]{bj.book}[/] [dim]({bj.completed_verses}/{bj.total_verses})[/]"706                        books_completed += 1707                    elif bj.status == JobStatus.FAILED:708                        desc = f"[red]{bj.book}[/] [dim](failed)[/]"709                        books_completed += 1710                    else:711                        desc = f"[dim]{bj.book}[/]"712713                    progress.update(task_id, description=desc)714715                # Count completed/failed books for overall progress716                if bj.status in (JobStatus.COMPLETED, JobStatus.FAILED):717                    books_completed += 1718719            # Update overall progress720            progress.update(overall_task, completed=books_completed)721722            time.sleep(0.5)  # Poll every 500ms723724        # Final update725        book_jobs = state_store.get_book_jobs(job_id)726        books_completed = sum(727            1 for bj in book_jobs728            if bj.status in (JobStatus.COMPLETED, JobStatus.FAILED)729        )730        progress.update(overall_task, completed=books_completed)731732    # Check for errors733    if error_container:734        raise error_container[0]735736    return result_container[0] if result_container else None737738739def bible_cmd(args: argparse.Namespace) -> int:740    """Handle the bible command for full Bible translation."""741    from src.orchestration import JobOrchestrator742    from src.orchestration.models import ALL_BOOKS, NEW_TESTAMENT_BOOKS, OLD_TESTAMENT_BOOKS743744    data_dir = get_data_dir()745746    # Determine which books to translate747    books = None748    if args.books:749        books = [b.lower() for b in args.books]750751    if args.dry_run:752        # Show what would be translated753        if books:754            selected = books755        elif args.nt_only:756            selected = list(NEW_TESTAMENT_BOOKS)757        elif args.ot_only:758            selected = list(OLD_TESTAMENT_BOOKS)759        else:760            selected = list(ALL_BOOKS)761762        console.print(f"[cyan]Would translate {len(selected)} books:[/cyan]")763        for book in selected:764            console.print(f"  - {book}")765        return 0766767    try:768        orchestrator = JobOrchestrator(769            data_dir=data_dir,770            max_parallel_books=args.parallel,771        )772773        # Create job774        job_id = orchestrator.create_job(775            books=books,776            nt_only=args.nt_only,777            ot_only=args.ot_only,778        )779780        # Get selected books for progress display781        if books:782            selected_books = books783        elif args.nt_only:784            selected_books = list(NEW_TESTAMENT_BOOKS)785        elif args.ot_only:786            selected_books = list(OLD_TESTAMENT_BOOKS)787        else:788            selected_books = list(ALL_BOOKS)789790        console.print(f"[cyan]Created job:[/cyan] {job_id}")791        console.print(f"[cyan]Books to translate:[/cyan] {len(selected_books)}")792        console.print(f"[cyan]Parallel workers:[/cyan] {args.parallel}")793        console.print()794795        # Run with progress bars796        result = run_with_progress(orchestrator, job_id, selected_books)797798        # Show results799        if result.success:800            console.print("\n[green]Translation completed successfully![/green]")801        else:802            console.print("\n[yellow]Translation completed with errors[/yellow]")803804        table = Table(title="Translation Results")805        table.add_column("Metric", style="cyan")806        table.add_column("Value", style="green")807808        table.add_row("Job ID", result.job_id)809        table.add_row("Books Completed", str(result.books_completed))810        table.add_row("Books Failed", str(result.books_failed))811        table.add_row("Verses Translated", str(result.total_verses_translated))812        table.add_row("Verses Failed", str(result.total_verses_failed))813        table.add_row("Duration", f"{result.duration_seconds:.1f}s")814815        console.print(table)816817        # Show failed books if any818        failed_books = [r for r in result.book_results if not r.success]819        if failed_books:820            console.print("\n[red]Failed books:[/red]")821            for book_result in failed_books:822                console.print(f"  - {book_result.book}: {book_result.error_message}")823824        return 0 if result.success else 1825826    except ValueError as e:827        console.print(f"[red]Error:[/red] {e}")828        return 1829    except KeyboardInterrupt:830        console.print("\n[yellow]Translation interrupted. Use 'resume' to continue.[/yellow]")831        return 1832833834def resume_cmd(args: argparse.Namespace) -> int:835    """Handle the resume command."""836    from src.orchestration import JobOrchestrator837838    data_dir = get_data_dir()839840    try:841        orchestrator = JobOrchestrator(842            data_dir=data_dir,843            max_parallel_books=args.parallel,844        )845846        job_id = args.job_id if hasattr(args, 'job_id') and args.job_id else None847848        # Get the job to determine books849        if job_id:850            job = orchestrator.state_store.get_bible_job(job_id)851            if not job:852                console.print(f"[red]Job not found:[/red] {job_id}")853                return 1854        else:855            job = orchestrator.state_store.get_latest_incomplete_job()856            if not job:857                console.print("[red]No incomplete jobs found[/red]")858                return 1859860        console.print(f"[cyan]Resuming job:[/cyan] {job.job_id}")861        console.print(f"[cyan]Books:[/cyan] {len(job.books)}")862        console.print(f"[cyan]Parallel workers:[/cyan] {args.parallel}")863        console.print()864865        # Reset orphaned verses866        reset_count = orchestrator.state_store.reset_orphaned_verses(job.job_id)867        if reset_count > 0:868            console.print(f"[yellow]Reset {reset_count} orphaned verses[/yellow]")869870        # Run with progress bars871        result = run_with_progress(orchestrator, job.job_id, job.books)872873        # Show results874        if result.success:875            console.print("\n[green]Translation completed successfully![/green]")876        else:877            console.print("\n[yellow]Translation completed with errors[/yellow]")878879        table = Table(title="Translation Results")880        table.add_column("Metric", style="cyan")881        table.add_column("Value", style="green")882883        table.add_row("Job ID", result.job_id)884        table.add_row("Books Completed", str(result.books_completed))885        table.add_row("Books Failed", str(result.books_failed))886        table.add_row("Verses Translated", str(result.total_verses_translated))887        table.add_row("Verses Failed", str(result.total_verses_failed))888        table.add_row("Duration", f"{result.duration_seconds:.1f}s")889890        console.print(table)891892        return 0 if result.success else 1893894    except ValueError as e:895        console.print(f"[red]Error:[/red] {e}")896        return 1897    except KeyboardInterrupt:898        console.print("\n[yellow]Translation interrupted. Use 'resume' to continue.[/yellow]")899        return 1900901902def job_status_cmd(args: argparse.Namespace) -> int:903    """Handle the job-status command."""904    from src.orchestration import JobOrchestrator905906    data_dir = get_data_dir()907908    try:909        orchestrator = JobOrchestrator(data_dir=data_dir)910911        status = orchestrator.get_status(args.job_id)912913        # Main job info914        table = Table(title=f"Job Status: {status['job_id'][:8]}...")915        table.add_column("Metric", style="cyan")916        table.add_column("Value", style="green")917918        table.add_row("Status", status["status"])919        table.add_row("Created", status["created_at"])920        table.add_row("Started", status["started_at"] or "Not started")921        table.add_row("Completed", status["completed_at"] or "In progress")922        table.add_row("Total Books", str(status["total_books"]))923        table.add_row("Overall Progress", f"{status['overall_progress']:.1f}%")924925        console.print(table)926927        # Book details928        book_table = Table(title="Book Progress")929        book_table.add_column("Book", style="cyan")930        book_table.add_column("Status", style="white")931        book_table.add_column("Progress", style="green")932        book_table.add_column("Completed", style="green")933        book_table.add_column("Failed", style="red")934935        for book in status["books"]:936            status_style = {937                "pending": "dim",938                "in_progress": "yellow",939                "completed": "green",940                "failed": "red",941            }.get(book["status"], "white")942943            book_table.add_row(944                book["book"],945                f"[{status_style}]{book['status']}[/{status_style}]",946                f"{book['progress']:.1f}%",947                str(book["completed_verses"]),948                str(book["failed_verses"]),949            )950951        console.print(book_table)952953        return 0954955    except ValueError as e:956        console.print(f"[red]Error:[/red] {e}")957        return 1958959960def jobs_list_cmd(args: argparse.Namespace) -> int:961    """Handle the jobs list command."""962    from src.orchestration.state import JobStateStore963964    data_dir = get_data_dir()965    db_path = data_dir / "jobs" / "state.db"966967    if not db_path.exists():968        console.print("[yellow]No job database found[/yellow]")969        return 0970971    state_store = JobStateStore(db_path)972    jobs = state_store.list_jobs(limit=args.limit)973974    if not jobs:975        console.print("[yellow]No jobs found[/yellow]")976        return 0977978    table = Table(title="Translation Jobs")979    table.add_column("Job ID", style="cyan")980    table.add_column("Status", style="white")981    table.add_column("Books", style="green")982    table.add_column("Progress", style="yellow")983    table.add_column("Created", style="dim")984985    for job in jobs:986        status_style = {987            "pending": "dim",988            "in_progress": "yellow",989            "completed": "green",990            "failed": "red",991        }.get(job.status.value, "white")992993        progress = 0994        if job.total_verses > 0:995            progress = job.completed_verses / job.total_verses * 100996997        table.add_row(998            job.job_id[:8] + "...",999            f"[{status_style}]{job.status.value}[/{status_style}]",1000            str(len(job.books)),1001            f"{progress:.1f}%",1002            job.created_at.strftime("%Y-%m-%d %H:%M"),1003        )10041005    console.print(table)10061007    return 0100810091010def jobs_clean_cmd(args: argparse.Namespace) -> int:1011    """Handle the jobs clean command."""1012    from src.orchestration.state import JobStateStore10131014    data_dir = get_data_dir()1015    db_path = data_dir / "jobs" / "state.db"10161017    if not db_path.exists():1018        console.print("[yellow]No job database found[/yellow]")1019        return 010201021    state_store = JobStateStore(db_path)10221023    # Delete specific job1024    if args.job_id:1025        if state_store.delete_job(args.job_id):1026            console.print(f"[green]Deleted job:[/green] {args.job_id}")1027        else:1028            console.print(f"[red]Job not found:[/red] {args.job_id}")1029            return 11030        return 010311032    # Delete old jobs1033    if args.older_than_days:1034        count = state_store.delete_jobs_older_than(args.older_than_days)1035        console.print(f"[green]Deleted {count} jobs older than {args.older_than_days} days[/green]")1036        return 010371038    # Delete completed jobs (keep latest N)1039    keep = args.keep_latest if args.keep_latest is not None else 11040    count = state_store.delete_completed_jobs(keep_latest=keep)1041    console.print(f"[green]Deleted {count} completed jobs (kept {keep} latest)[/green]")10421043    return 0104410451046def jobs_stats_cmd(args: argparse.Namespace) -> int:1047    """Handle the jobs stats command."""1048    from src.orchestration.state import JobStateStore10491050    data_dir = get_data_dir()1051    db_path = data_dir / "jobs" / "state.db"10521053    if not db_path.exists():1054        console.print("[yellow]No job database found[/yellow]")1055        return 010561057    state_store = JobStateStore(db_path)1058    stats = state_store.get_database_stats()10591060    table = Table(title="Job Database Statistics")1061    table.add_column("Metric", style="cyan")1062    table.add_column("Value", style="green")10631064    table.add_row("Total Jobs", str(stats["total_jobs"]))1065    table.add_row("Pending Jobs", str(stats["pending_jobs"]))1066    table.add_row("In Progress Jobs", str(stats["in_progress_jobs"]))1067    table.add_row("Completed Jobs", str(stats["completed_jobs"]))1068    table.add_row("Failed Jobs", str(stats["failed_jobs"]))1069    table.add_row("Total Verse Records", str(stats["total_verse_records"]))1070    table.add_row("Database Size", f"{stats['database_size_mb']:.2f} MB")10711072    console.print(table)10731074    return 0107510761077def main() -> int:1078    """Main entry point."""1079    parser = argparse.ArgumentParser(1080        description="Japanese Bible Translator - Machine-generated modern Japanese translation"1081    )10821083    subparsers = parser.add_subparsers(dest="command", help="Available commands")10841085    originals_parser = subparsers.add_parser("build-originals", help="Build original-language parallel assets offline")1086    originals_parser.add_argument("--out", type=Path)1087    alignment_parser = subparsers.add_parser("audit-alignment", help="Build the reviewed numbering candidate and unresolved queue offline")1088    alignment_parser.add_argument("--source", type=Path, default=get_data_dir() / "output")1089    alignment_parser.add_argument("--review", type=Path, default=Path(__file__).parent / "reports/nichijo-v1-numbering-review.json")1090    alignment_parser.add_argument("--out", type=Path, default=Path(__file__).parent / "reports/nichijo-v1-numbering-alignment.json")1091    aligned_export_parser = subparsers.add_parser("export-aligned", help="Export a complete reviewed 日常訳第1版 release offline")1092    aligned_export_parser.add_argument("--source", type=Path, default=get_data_dir() / "output")1093    aligned_export_parser.add_argument("--audit", type=Path, default=Path(__file__).parent / "reports/nichijo-v1-numbering-alignment.json")1094    aligned_export_parser.add_argument("--out", type=Path, required=True)1095    repair_import_parser = subparsers.add_parser("import-alignment-repairs", help="Import a verified repair evidence bundle offline")1096    repair_import_parser.add_argument("--review", type=Path, required=True)1097    repair_import_parser.add_argument("--bundle", type=Path, required=True)1098    repair_import_parser.add_argument("--out", type=Path, required=True)10991100    # Verse translation1101    verse_parser = subparsers.add_parser("verse", help="Translate a single verse")1102    verse_parser.add_argument("verse", help="Verse reference (e.g., 'john:1:1')")1103    verse_parser.add_argument(1104        "-f", "--force", action="store_true", help="Re-translate even if exists"1105    )1106    verse_parser.add_argument(1107        "-v", "--validate", action="store_true", help="Validate the translation"1108    )1109    verse_parser.add_argument(1110        "--no-evaluate", action="store_true", help="Disable automatic evaluation"1111    )11121113    # Chapter translation1114    chapter_parser = subparsers.add_parser("chapter", help="Translate a chapter")1115    chapter_parser.add_argument("book", help="Book name (e.g., 'john')")1116    chapter_parser.add_argument("chapter", type=int, help="Chapter number")1117    chapter_parser.add_argument(1118        "-f", "--force", action="store_true", help="Re-translate existing"1119    )1120    chapter_parser.add_argument(1121        "--no-evaluate", action="store_true", help="Disable automatic evaluation"1122    )11231124    # Book translation1125    book_parser = subparsers.add_parser("book", help="Translate an entire book")1126    book_parser.add_argument("book", help="Book name (e.g., 'john')")1127    book_parser.add_argument(1128        "-f", "--force", action="store_true", help="Re-translate existing"1129    )1130    book_parser.add_argument(1131        "--no-evaluate", action="store_true", help="Disable automatic evaluation"1132    )11331134    # Export1135    export_parser = subparsers.add_parser("export", help="Export translations")1136    export_parser.add_argument("book", help="Book name to export")1137    export_parser.add_argument(1138        "formats", nargs="+", choices=["json", "text", "html"], help="Output formats"1139    )1140    export_parser.add_argument("-o", "--output", help="Output directory")11411142    # Status1143    status_parser = subparsers.add_parser("status", help="Show translation status")1144    status_parser.add_argument("book", help="Book name")11451146    # Evaluate existing translations1147    evaluate_parser = subparsers.add_parser(1148        "evaluate", help="Evaluate existing translations"1149    )1150    evaluate_parser.add_argument("book", help="Book name")1151    evaluate_parser.add_argument("--chapter", type=int, required=True, help="Chapter")11521153    # Style configuration1154    style_parser = subparsers.add_parser("style", help="Manage translation style")1155    style_subparsers = style_parser.add_subparsers(dest="style_command")11561157    style_show_parser = style_subparsers.add_parser("show", help="Show current style")11581159    style_edit_parser = style_subparsers.add_parser("edit", help="Edit style settings")1160    style_edit_parser.add_argument(1161        "--equivalence",1162        choices=["formal", "dynamic", "optimal"],1163        help="Equivalence approach",1164    )1165    style_edit_parser.add_argument(1166        "--gender",1167        choices=["traditional", "moderate_inclusive", "fully_inclusive"],1168        help="Gender language",1169    )1170    style_edit_parser.add_argument(1171        "--readability",1172        choices=["scholarly", "general_public", "web_native", "children"],1173        help="Readability level",1174    )1175    style_edit_parser.add_argument(1176        "--tradition",1177        choices=["evangelical", "catholic", "orthodox", "ecumenical"],1178        help="Theological tradition",1179    )1180    style_edit_parser.add_argument(1181        "--consistency",1182        choices=["strict", "balanced", "contextual"],1183        help="Term consistency",1184    )1185    style_edit_parser.add_argument(1186        "--formality",1187        choices=["casual", "standard", "polite", "formal"],1188        help="Formality level",1189    )11901191    # Translation Memory status1192    tm_parser = subparsers.add_parser("tm", help="Translation Memory management")1193    tm_subparsers = tm_parser.add_subparsers(dest="tm_command")1194    tm_status_parser = tm_subparsers.add_parser(1195        "status", help="Show TM statistics"1196    )11971198    # Pericope commands1199    pericope_parser = subparsers.add_parser(1200        "pericope", help="Pericope-based (chunk) translation"1201    )1202    pericope_subparsers = pericope_parser.add_subparsers(dest="pericope_command")12031204    # pericope list1205    pericope_list_parser = pericope_subparsers.add_parser(1206        "list", help="List pericopes for a book"1207    )1208    pericope_list_parser.add_argument("book", help="Book name (e.g., 'john')")12091210    # pericope translate1211    pericope_translate_parser = pericope_subparsers.add_parser(1212        "translate", help="Translate a specific pericope"1213    )1214    pericope_translate_parser.add_argument(1215        "pericope_id", help="Pericope ID (e.g., 'john-1-prologue')"1216    )1217    pericope_translate_parser.add_argument(1218        "-f", "--force", action="store_true", help="Re-translate even if exists"1219    )12201221    # pericope chapter1222    pericope_chapter_parser = pericope_subparsers.add_parser(1223        "chapter", help="Translate a chapter by pericopes"1224    )1225    pericope_chapter_parser.add_argument("book", help="Book name (e.g., 'john')")1226    pericope_chapter_parser.add_argument("chapter", type=int, help="Chapter number")1227    pericope_chapter_parser.add_argument(1228        "-f", "--force", action="store_true", help="Re-translate existing"1229    )12301231    # pericope stats1232    pericope_stats_parser = pericope_subparsers.add_parser(1233        "stats", help="Show pericope data statistics"1234    )12351236    # TUI1237    tui_parser = subparsers.add_parser("tui", help="Launch the TUI viewer")1238    tui_parser.add_argument("-d", "--data-dir", help="Data directory (default: data)")12391240    # Full Bible translation1241    bible_parser = subparsers.add_parser(1242        "bible", help="Translate full Bible with parallel processing"1243    )1244    bible_parser.add_argument(1245        "--books", nargs="+", help="Specific books to translate (default: all 66)"1246    )1247    bible_parser.add_argument(1248        "--nt-only", action="store_true", help="Only translate New Testament (27 books)"1249    )1250    bible_parser.add_argument(1251        "--ot-only", action="store_true", help="Only translate Old Testament (39 books)"1252    )1253    bible_parser.add_argument(1254        "-p", "--parallel", type=int, default=4,1255        help="Number of parallel book workers (default: 4)"1256    )1257    bible_parser.add_argument(1258        "--dry-run", action="store_true", help="Show what would be translated"1259    )12601261    # Resume incomplete job1262    resume_parser = subparsers.add_parser(1263        "resume", help="Resume an incomplete translation job"1264    )1265    resume_parser.add_argument(1266        "--job-id", help="Specific job ID to resume (default: latest incomplete)"1267    )1268    resume_parser.add_argument(1269        "-p", "--parallel", type=int, default=4,1270        help="Number of parallel book workers (default: 4)"1271    )12721273    # Job status1274    job_status_parser = subparsers.add_parser(1275        "job-status", help="Show status of a translation job"1276    )1277    job_status_parser.add_argument("job_id", help="Job ID to check")12781279    # Jobs management1280    jobs_parser = subparsers.add_parser(1281        "jobs", help="Manage translation jobs and database cleanup"1282    )1283    jobs_subparsers = jobs_parser.add_subparsers(dest="jobs_command")12841285    # jobs list1286    jobs_list_parser = jobs_subparsers.add_parser(1287        "list", help="List all translation jobs"1288    )1289    jobs_list_parser.add_argument(1290        "-n", "--limit", type=int, default=20,1291        help="Maximum number of jobs to show (default: 20)"1292    )12931294    # jobs clean1295    jobs_clean_parser = jobs_subparsers.add_parser(1296        "clean", help="Clean old jobs from database"1297    )1298    jobs_clean_parser.add_argument(1299        "--job-id", help="Delete a specific job by ID"1300    )1301    jobs_clean_parser.add_argument(1302        "--older-than-days", type=int,1303        help="Delete jobs older than this many days"1304    )1305    jobs_clean_parser.add_argument(1306        "--keep-latest", type=int,1307        help="Keep the N most recent completed jobs (default: 1)"1308    )13091310    # jobs stats1311    jobs_stats_parser = jobs_subparsers.add_parser(1312        "stats", help="Show database statistics"1313    )13141315    args = parser.parse_args()13161317    if args.command == "build-originals":1318        from src.output.originals import WEB, build1319        report = build(args.out or WEB / "public/originals")1320        console.print(f"Built {len(report['files'])} original-language books")1321        return 01322    elif args.command == "audit-alignment":1323        from src.output.alignment import build_audit1324        report = build_audit(args.source, args.review, args.out)1325        console.print(report['summary'])1326        return 0 if report['numbering_ready'] else 11327    elif args.command == "export-aligned":1328        from src.output.alignment import export_aligned1329        console.print(export_aligned(args.source, args.audit, args.out))1330        return 01331    elif args.command == "import-alignment-repairs":1332        from src.output.repair_review import import_bundle1333        count = import_bundle(args.review, args.bundle, args.out)1334        console.print(f"Imported {count} verified target repairs")1335        return 01336    elif args.command == "verse":1337        return translate_verse_cmd(args)1338    elif args.command == "chapter":1339        return translate_chapter_cmd(args)1340    elif args.command == "book":1341        return translate_book_cmd(args)1342    elif args.command == "export":1343        return export_cmd(args)1344    elif args.command == "status":1345        return status_cmd(args)1346    elif args.command == "evaluate":1347        return evaluate_cmd(args)1348    elif args.command == "style":1349        if args.style_command == "show":1350            return style_show_cmd(args)1351        elif args.style_command == "edit":1352            return style_edit_cmd(args)1353        else:1354            return style_show_cmd(args)1355    elif args.command == "tm":1356        if args.tm_command == "status":1357            return tm_status_cmd(args)1358        else:1359            return tm_status_cmd(args)1360    elif args.command == "pericope":1361        if args.pericope_command == "list":1362            return pericope_list_cmd(args)1363        elif args.pericope_command == "translate":1364            return pericope_translate_cmd(args)1365        elif args.pericope_command == "chapter":1366            return pericope_chapter_cmd(args)1367        elif args.pericope_command == "stats":1368            return pericope_stats_cmd(args)1369        else:1370            return pericope_stats_cmd(args)1371    elif args.command == "tui":1372        return tui_cmd(args)1373    elif args.command == "bible":1374        return bible_cmd(args)1375    elif args.command == "resume":1376        return resume_cmd(args)1377    elif args.command == "job-status":1378        return job_status_cmd(args)1379    elif args.command == "jobs":1380        if args.jobs_command == "list":1381            return jobs_list_cmd(args)1382        elif args.jobs_command == "clean":1383            return jobs_clean_cmd(args)1384        elif args.jobs_command == "stats":1385            return jobs_stats_cmd(args)1386        else:1387            return jobs_list_cmd(args)1388    else:1389        parser.print_help()1390        return 0139113921393if __name__ == "__main__":1394    sys.exit(main())1395