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