"""Japanese Bible Translator - Main entry point.""" from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: from src.orchestration import JobOrchestrator from src.orchestration.models import JobResult from rich.console import Console from rich.live import Live from rich.progress import ( BarColumn, MofNCompleteColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn, ) from rich.table import Table from src.config.style_config import StyleConfigManager from src.context.builder import ContextBuilder from src.context.glossary import GlossaryManager from src.context.pericope import PericopeManager from src.context.translation_memory import TranslationMemoryManager from src.data.loader import DataLoader from src.data.storage import Storage from src.evaluation.evaluator import TranslationEvaluator from src.evaluation.storage import EvaluationStorage from src.models.pericope import Pericope from src.models.style import TranslationStyle from src.models.verse import VerseReference from src.output.formatters import OutputFormatter from src.pipeline.prompt import PromptBuilder from src.pipeline.translator import DEFAULT_MODEL, Translator from src.validation.checks import TranslationValidator console = Console() def tui_cmd(args: argparse.Namespace) -> int: """Handle the tui command.""" from src.tui.app import run_tui data_dir = Path(args.data_dir) if args.data_dir else get_data_dir() run_tui(data_dir) return 0 def get_data_dir() -> Path: """Get the data directory path.""" return Path(__file__).parent / "data" def create_translator( auto_evaluate: bool = True, use_tm: bool = True, ) -> Translator: """Create and configure the translator. Args: auto_evaluate: Whether to enable auto-evaluation (default: True). use_tm: Whether to enable translation memory. Returns: Configured Translator instance. """ data_dir = get_data_dir() loader = DataLoader(data_dir) storage = Storage(data_dir) # Load glossary glossary_terms = loader.load_glossary() glossary = GlossaryManager(glossary_terms) # Load style configuration style_manager = StyleConfigManager(data_dir / "config" / "style.json") style = style_manager.get_style() # Create TM if enabled tm = None if use_tm: tm = TranslationMemoryManager(data_dir / "memory" / "segments.json") # Create pericope manager pericope_manager = PericopeManager(data_dir / "metadata" / "pericopes.json") # Create components context_builder = ContextBuilder(loader, glossary, pericope_manager=pericope_manager) prompt_builder = PromptBuilder(glossary, style=style, translation_memory=tm) # Always create evaluator and storage (auto_evaluate defaults to True) evaluator = TranslationEvaluator(data_dir=data_dir) eval_storage = EvaluationStorage(data_dir) return Translator( context_builder, prompt_builder, storage, translation_memory=tm, auto_evaluate=auto_evaluate, evaluator=evaluator, evaluation_storage=eval_storage, ) def translate_verse_cmd(args: argparse.Namespace) -> int: """Handle the verse command.""" try: ref = VerseReference.from_string(args.verse) except ValueError as e: console.print(f"[red]Error:[/red] {e}") return 1 console.print(f"Translating {ref}...") try: # Evaluations are automatic by default, --no-evaluate disables auto_eval = not getattr(args, 'no_evaluate', False) translator = create_translator(auto_evaluate=auto_eval) output = translator.translate_verse(ref, skip_existing=not args.force) console.print(f"\n[green]Translation:[/green]") console.print(f" {output.text}") console.print(f"\n[dim]Model: {output.model}[/dim]") # Validate if args.validate: validator = TranslationValidator() report = validator.validate(output) if not report.all_passed: console.print("\n[yellow]Validation warnings:[/yellow]") for result in report.failed_checks: console.print(f" - {result.message}") if auto_eval: console.print("[dim]Evaluation saved[/dim]") except ValueError as e: console.print(f"[red]Error:[/red] {e}") return 1 return 0 def translate_chapter_cmd(args: argparse.Namespace) -> int: """Handle the chapter command.""" book = args.book chapter = args.chapter console.print(f"Translating {book.title()} chapter {chapter}...") try: # Evaluations are automatic by default, --no-evaluate disables auto_eval = not getattr(args, 'no_evaluate', False) translator = create_translator(auto_evaluate=auto_eval) def progress_callback(current: int, total: int, ref: VerseReference) -> None: console.print(f" [{current}/{total}] {ref}") outputs = translator.translate_chapter( book, chapter, skip_existing=not args.force, progress_callback=progress_callback, ) console.print(f"\n[green]Completed {len(outputs)} verses[/green]") if auto_eval: console.print("[dim]Evaluations saved[/dim]") except ValueError as e: console.print(f"[red]Error:[/red] {e}") return 1 return 0 def translate_book_cmd(args: argparse.Namespace) -> int: """Handle the book command.""" book = args.book console.print(f"Translating {book.title()}...") try: # Evaluations are automatic by default, --no-evaluate disables auto_eval = not getattr(args, 'no_evaluate', False) translator = create_translator(auto_evaluate=auto_eval) loader = DataLoader(get_data_dir()) chapters = loader.get_book_chapters(book) if not chapters: console.print(f"[yellow]No chapters found for {book}[/yellow]") return 1 total_verses = 0 with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console, ) as progress: for chapter in chapters: task = progress.add_task(f"Chapter {chapter}...", total=None) outputs = translator.translate_chapter( book, chapter, skip_existing=not args.force ) total_verses += len(outputs) progress.remove_task(task) console.print(f"\n[green]Completed {total_verses} verses[/green]") if auto_eval: console.print("[dim]Evaluations saved[/dim]") except ValueError as e: console.print(f"[red]Error:[/red] {e}") return 1 return 0 def export_cmd(args: argparse.Namespace) -> int: """Handle the export command.""" data_dir = get_data_dir() storage = Storage(data_dir) formatter = OutputFormatter(storage) output_dir = Path(args.output) if args.output else data_dir / "exports" book = args.book formats = args.formats console.print(f"Exporting {book}...") for fmt in formats: if fmt == "json": path = formatter.export_book_json(book, output_dir) console.print(f" [green]JSON:[/green] {path}") elif fmt == "text": path = formatter.export_book_text(book, output_dir) console.print(f" [green]Text:[/green] {path}") elif fmt == "html": # Export each chapter as HTML loader = DataLoader(data_dir) metadata = loader.load_book_metadata(book) chapters = loader.get_book_chapters(book) for chapter in chapters: path = formatter.export_chapter_html( book, chapter, output_dir / "html", metadata ) console.print(f" [green]HTML:[/green] {output_dir / 'html'}") return 0 def status_cmd(args: argparse.Namespace) -> int: """Handle the status command.""" data_dir = get_data_dir() storage = Storage(data_dir) loader = DataLoader(data_dir) book = args.book # Get book metadata metadata = loader.load_book_metadata(book) total_verses = metadata.verse_count if metadata else 0 if total_verses == 0: # Estimate from source files chapters = loader.get_book_chapters(book) for chapter in chapters: verses = loader.load_chapter_verses(book, chapter) total_verses += len(verses) stats = storage.get_completion_stats(book, total_verses) table = Table(title=f"{book.title()} Translation Status") table.add_column("Metric", style="cyan") table.add_column("Value", style="green") table.add_row("Completed verses", str(stats["completed"])) table.add_row("Total verses", str(stats["total"])) table.add_row("Progress", f"{stats['percentage']}%") table.add_row("Chapters started", str(stats["chapters_started"])) console.print(table) return 0 def evaluate_cmd(args: argparse.Namespace) -> int: """Handle the evaluate command.""" from src.evaluation.evaluator import ( EVALUATION_SYSTEM_PROMPT, TranslationEvaluator, parse_evaluation_response, ) data_dir = get_data_dir() loader = DataLoader(data_dir) storage = Storage(data_dir) eval_storage = EvaluationStorage(data_dir) # Load glossary glossary_terms = loader.load_glossary() glossary = GlossaryManager(glossary_terms) context_builder = ContextBuilder(loader, glossary) evaluator = TranslationEvaluator(data_dir=data_dir) book = args.book chapter = args.chapter console.print(f"Evaluating {book.title()} chapter {chapter}...") try: verses = loader.load_chapter_verses(book, chapter) verse_evaluations = {} for i, verse in enumerate(verses): ref = verse.reference console.print(f" [{i + 1}/{len(verses)}] {ref}") # Load translation translation = storage.load_translation(ref) if not translation: console.print(f" [yellow]No translation found, skipping[/yellow]") continue # Build context and evaluation prompt context = context_builder.build_context(ref) eval_prompt = evaluator.build_verse_prompt(translation.text, context) # Call Claude CLI for evaluation full_prompt = f"{EVALUATION_SYSTEM_PROMPT}\n\n{eval_prompt}" try: import subprocess result = subprocess.run( ["claude", "--print", "-p", full_prompt, "--model", DEFAULT_MODEL], capture_output=True, text=True, check=False, timeout=120, ) if result.returncode != 0: raise RuntimeError(f"Claude CLI error: {result.stderr}") eval_result = result.stdout except Exception as e: console.print(f" [red]Evaluation failed: {e}[/red]") continue try: scores = parse_evaluation_response(eval_result.strip()) evaluation = evaluator.create_verse_evaluation( verse_number=ref.verse, scores=scores ) verse_evaluations[ref.verse] = evaluation except ValueError as e: console.print(f" [red]Parse error: {e}[/red]") continue # Create and save chapter evaluation if verse_evaluations: chapter_eval = evaluator.create_chapter_evaluation( book, chapter, verse_evaluations ) eval_storage.save_chapter_evaluation(chapter_eval) console.print( f"\n[green]Evaluated {len(verse_evaluations)} verses[/green]" ) console.print( f"Average score: {chapter_eval.overall_score:.1f}" ) else: console.print("[yellow]No verses evaluated[/yellow]") except ValueError as e: console.print(f"[red]Error:[/red] {e}") return 1 return 0 def style_show_cmd(args: argparse.Namespace) -> int: """Handle the style show command.""" data_dir = get_data_dir() manager = StyleConfigManager(data_dir / "config" / "style.json") style = manager.get_style() table = Table(title="Translation Style Configuration") table.add_column("Setting", style="cyan") table.add_column("Value", style="green") table.add_row("Equivalence Approach", style.equivalence_approach.value) table.add_row("Gender Language", style.gender_language.value) table.add_row("Readability Level", style.readability_level.value) table.add_row("Theological Tradition", style.theological_tradition.value) table.add_row("Textual Basis", style.textual_basis) table.add_row("Term Consistency", style.term_consistency.value) table.add_row("Formality Level", style.formality_level.value) table.add_row("Avoid Archaic", ", ".join(style.avoid_archaic)) table.add_row("Target Style Reference", style.target_style_reference) if style.additional_instructions: table.add_row("Additional Instructions", style.additional_instructions) console.print(table) console.print(f"\n[dim]Config file: {manager.config_path}[/dim]") return 0 def style_edit_cmd(args: argparse.Namespace) -> int: """Handle the style edit command.""" data_dir = get_data_dir() manager = StyleConfigManager(data_dir / "config" / "style.json") style = manager.get_style() # Apply changes from arguments changed = False if args.equivalence: from src.models.style import EquivalenceApproach style.equivalence_approach = EquivalenceApproach(args.equivalence) changed = True if args.gender: from src.models.style import GenderLanguage style.gender_language = GenderLanguage(args.gender) changed = True if args.readability: from src.models.style import ReadabilityLevel style.readability_level = ReadabilityLevel(args.readability) changed = True if args.tradition: from src.models.style import TheologicalTradition style.theological_tradition = TheologicalTradition(args.tradition) changed = True if args.consistency: from src.models.style import TermConsistency style.term_consistency = TermConsistency(args.consistency) changed = True if args.formality: from src.models.style import FormalityLevel style.formality_level = FormalityLevel(args.formality) changed = True if changed: manager.save(style) console.print("[green]Style configuration updated[/green]") return style_show_cmd(args) else: console.print("[yellow]No changes specified[/yellow]") console.print("Use --help to see available options") return 1 def tm_status_cmd(args: argparse.Namespace) -> int: """Handle the tm status command.""" data_dir = get_data_dir() tm = TranslationMemoryManager(data_dir / "memory" / "segments.json") stats = tm.get_statistics() table = Table(title="Translation Memory Status") table.add_column("Metric", style="cyan") table.add_column("Value", style="green") table.add_row("Total Segments", str(stats.total_segments)) table.add_row("Verse Segments", str(stats.verse_segments)) table.add_row("Phrase Segments", str(stats.phrase_segments)) table.add_row("Total Reuses", str(stats.total_reuses)) table.add_row("Average Reuse", f"{stats.average_reuse:.2f}") table.add_row("Books Covered", ", ".join(stats.books_covered) or "None") console.print(table) return 0 def pericope_list_cmd(args: argparse.Namespace) -> int: """Handle the pericope list command.""" data_dir = get_data_dir() pericope_manager = PericopeManager(data_dir / "metadata" / "pericopes.json") book = args.book.lower() if not pericope_manager.has_pericope_data(book): console.print(f"[yellow]No pericope data available for {book}[/yellow]") console.print("Run 'uv run python scripts/fetch_pericope_data.py --auto-generate' to generate") return 1 pericopes = pericope_manager.get_book_pericopes(book) table = Table(title=f"Pericopes for {book.title()}") table.add_column("ID", style="cyan") table.add_column("Range", style="green") table.add_column("Verses", style="yellow") table.add_column("Title", style="white") for p in pericopes: table.add_row( p.id, p.get_display_range(), str(p.verse_count), p.title or "[auto-generated]", ) console.print(table) console.print(f"\n[dim]Total: {len(pericopes)} pericopes[/dim]") return 0 def pericope_translate_cmd(args: argparse.Namespace) -> int: """Handle the pericope translate command.""" pericope_id = args.pericope_id console.print(f"Translating pericope {pericope_id}...") try: translator = create_translator() output = translator.translate_chunk_by_id( pericope_id, skip_existing=not args.force ) if output is None: console.print(f"[red]Pericope not found: {pericope_id}[/red]") return 1 console.print(f"\n[green]Translated {output.verse_count} verses:[/green]") # Show translations for verse_ref, text in sorted(output.translations.items()): console.print(f" [{verse_ref}] {text[:50]}..." if len(text) > 50 else f" [{verse_ref}] {text}") console.print(f"\n[dim]Model: {output.model}[/dim]") if output.coherence_note: console.print(f"[dim]Note: {output.coherence_note}[/dim]") except ValueError as e: console.print(f"[red]Error:[/red] {e}") return 1 return 0 def pericope_chapter_cmd(args: argparse.Namespace) -> int: """Handle the pericope chapter command.""" book = args.book.lower() chapter = args.chapter console.print(f"Translating {book.title()} chapter {chapter} by pericopes...") try: translator = create_translator() def progress_callback(current: int, total: int, pericope: Pericope) -> None: title = pericope.title or pericope.get_display_range() console.print(f" [{current}/{total}] {title}") outputs = translator.translate_chapter_by_pericopes( book, chapter, skip_existing=not args.force, progress_callback=progress_callback, ) total_verses = sum(o.verse_count for o in outputs) console.print(f"\n[green]Completed {len(outputs)} pericopes ({total_verses} verses)[/green]") except ValueError as e: console.print(f"[red]Error:[/red] {e}") return 1 return 0 def pericope_stats_cmd(args: argparse.Namespace) -> int: """Handle the pericope stats command.""" data_dir = get_data_dir() pericope_manager = PericopeManager(data_dir / "metadata" / "pericopes.json") stats = pericope_manager.get_statistics() table = Table(title="Pericope Data Statistics") table.add_column("Metric", style="cyan") table.add_column("Value", style="green") table.add_row("Books with Data", str(stats["books_with_data"])) table.add_row("Book Names", ", ".join(stats["book_names"]) or "None") table.add_row("Total Pericopes", str(stats["total_pericopes"])) table.add_row("Total Verses Covered", str(stats["total_verses_covered"])) console.print(table) return 0 def run_with_progress( orchestrator: "JobOrchestrator", job_id: str, books: list[str], ) -> "JobResult": """Run translation with live progress bars. Args: orchestrator: JobOrchestrator instance. job_id: Job ID to run. books: List of books to translate. Returns: JobResult from the orchestrator. """ import threading import time from src.orchestration.models import JobStatus from src.orchestration.state import JobStateStore result_container: list = [] error_container: list = [] def run_translation(): try: result = orchestrator.run(job_id) result_container.append(result) except Exception as e: error_container.append(e) # Start translation in background thread thread = threading.Thread(target=run_translation, daemon=True) thread.start() # Create progress bars progress = Progress( SpinnerColumn(), TextColumn("[bold blue]{task.description}"), BarColumn(bar_width=40), TaskProgressColumn(), MofNCompleteColumn(), TimeElapsedColumn(), console=console, expand=False, ) # Track book progress tasks book_tasks: dict[str, int] = {} overall_task = None with progress: # Overall progress bar overall_task = progress.add_task( "[yellow]Overall Progress", total=len(books), ) # Per-book progress bars (initially hidden until initialized) for book in books: book_tasks[book] = progress.add_task( f"[cyan]{book}", total=1, # Will be updated when verse count is known visible=False, ) # Poll for progress updates state_store = JobStateStore(orchestrator.data_dir / "jobs" / "state.db") while thread.is_alive(): # Get current book statuses book_jobs = state_store.get_book_jobs(job_id) books_completed = 0 for bj in book_jobs: task_id = book_tasks.get(bj.book) if task_id is None: continue # Update verse count if known if bj.total_verses > 0: progress.update( task_id, total=bj.total_verses, completed=bj.completed_verses, visible=True, ) # Update description with status if bj.status == JobStatus.IN_PROGRESS: desc = f"[cyan]{bj.book}[/] [dim]({bj.completed_verses}/{bj.total_verses})[/]" elif bj.status == JobStatus.COMPLETED: desc = f"[green]{bj.book}[/] [dim]({bj.completed_verses}/{bj.total_verses})[/]" books_completed += 1 elif bj.status == JobStatus.FAILED: desc = f"[red]{bj.book}[/] [dim](failed)[/]" books_completed += 1 else: desc = f"[dim]{bj.book}[/]" progress.update(task_id, description=desc) # Count completed/failed books for overall progress if bj.status in (JobStatus.COMPLETED, JobStatus.FAILED): books_completed += 1 # Update overall progress progress.update(overall_task, completed=books_completed) time.sleep(0.5) # Poll every 500ms # Final update book_jobs = state_store.get_book_jobs(job_id) books_completed = sum( 1 for bj in book_jobs if bj.status in (JobStatus.COMPLETED, JobStatus.FAILED) ) progress.update(overall_task, completed=books_completed) # Check for errors if error_container: raise error_container[0] return result_container[0] if result_container else None def bible_cmd(args: argparse.Namespace) -> int: """Handle the bible command for full Bible translation.""" from src.orchestration import JobOrchestrator from src.orchestration.models import ALL_BOOKS, NEW_TESTAMENT_BOOKS, OLD_TESTAMENT_BOOKS data_dir = get_data_dir() # Determine which books to translate books = None if args.books: books = [b.lower() for b in args.books] if args.dry_run: # Show what would be translated if books: selected = books elif args.nt_only: selected = list(NEW_TESTAMENT_BOOKS) elif args.ot_only: selected = list(OLD_TESTAMENT_BOOKS) else: selected = list(ALL_BOOKS) console.print(f"[cyan]Would translate {len(selected)} books:[/cyan]") for book in selected: console.print(f" - {book}") return 0 try: orchestrator = JobOrchestrator( data_dir=data_dir, max_parallel_books=args.parallel, ) # Create job job_id = orchestrator.create_job( books=books, nt_only=args.nt_only, ot_only=args.ot_only, ) # Get selected books for progress display if books: selected_books = books elif args.nt_only: selected_books = list(NEW_TESTAMENT_BOOKS) elif args.ot_only: selected_books = list(OLD_TESTAMENT_BOOKS) else: selected_books = list(ALL_BOOKS) console.print(f"[cyan]Created job:[/cyan] {job_id}") console.print(f"[cyan]Books to translate:[/cyan] {len(selected_books)}") console.print(f"[cyan]Parallel workers:[/cyan] {args.parallel}") console.print() # Run with progress bars result = run_with_progress(orchestrator, job_id, selected_books) # Show results if result.success: console.print("\n[green]Translation completed successfully![/green]") else: console.print("\n[yellow]Translation completed with errors[/yellow]") table = Table(title="Translation Results") table.add_column("Metric", style="cyan") table.add_column("Value", style="green") table.add_row("Job ID", result.job_id) table.add_row("Books Completed", str(result.books_completed)) table.add_row("Books Failed", str(result.books_failed)) table.add_row("Verses Translated", str(result.total_verses_translated)) table.add_row("Verses Failed", str(result.total_verses_failed)) table.add_row("Duration", f"{result.duration_seconds:.1f}s") console.print(table) # Show failed books if any failed_books = [r for r in result.book_results if not r.success] if failed_books: console.print("\n[red]Failed books:[/red]") for book_result in failed_books: console.print(f" - {book_result.book}: {book_result.error_message}") return 0 if result.success else 1 except ValueError as e: console.print(f"[red]Error:[/red] {e}") return 1 except KeyboardInterrupt: console.print("\n[yellow]Translation interrupted. Use 'resume' to continue.[/yellow]") return 1 def resume_cmd(args: argparse.Namespace) -> int: """Handle the resume command.""" from src.orchestration import JobOrchestrator data_dir = get_data_dir() try: orchestrator = JobOrchestrator( data_dir=data_dir, max_parallel_books=args.parallel, ) job_id = args.job_id if hasattr(args, 'job_id') and args.job_id else None # Get the job to determine books if job_id: job = orchestrator.state_store.get_bible_job(job_id) if not job: console.print(f"[red]Job not found:[/red] {job_id}") return 1 else: job = orchestrator.state_store.get_latest_incomplete_job() if not job: console.print("[red]No incomplete jobs found[/red]") return 1 console.print(f"[cyan]Resuming job:[/cyan] {job.job_id}") console.print(f"[cyan]Books:[/cyan] {len(job.books)}") console.print(f"[cyan]Parallel workers:[/cyan] {args.parallel}") console.print() # Reset orphaned verses reset_count = orchestrator.state_store.reset_orphaned_verses(job.job_id) if reset_count > 0: console.print(f"[yellow]Reset {reset_count} orphaned verses[/yellow]") # Run with progress bars result = run_with_progress(orchestrator, job.job_id, job.books) # Show results if result.success: console.print("\n[green]Translation completed successfully![/green]") else: console.print("\n[yellow]Translation completed with errors[/yellow]") table = Table(title="Translation Results") table.add_column("Metric", style="cyan") table.add_column("Value", style="green") table.add_row("Job ID", result.job_id) table.add_row("Books Completed", str(result.books_completed)) table.add_row("Books Failed", str(result.books_failed)) table.add_row("Verses Translated", str(result.total_verses_translated)) table.add_row("Verses Failed", str(result.total_verses_failed)) table.add_row("Duration", f"{result.duration_seconds:.1f}s") console.print(table) return 0 if result.success else 1 except ValueError as e: console.print(f"[red]Error:[/red] {e}") return 1 except KeyboardInterrupt: console.print("\n[yellow]Translation interrupted. Use 'resume' to continue.[/yellow]") return 1 def job_status_cmd(args: argparse.Namespace) -> int: """Handle the job-status command.""" from src.orchestration import JobOrchestrator data_dir = get_data_dir() try: orchestrator = JobOrchestrator(data_dir=data_dir) status = orchestrator.get_status(args.job_id) # Main job info table = Table(title=f"Job Status: {status['job_id'][:8]}...") table.add_column("Metric", style="cyan") table.add_column("Value", style="green") table.add_row("Status", status["status"]) table.add_row("Created", status["created_at"]) table.add_row("Started", status["started_at"] or "Not started") table.add_row("Completed", status["completed_at"] or "In progress") table.add_row("Total Books", str(status["total_books"])) table.add_row("Overall Progress", f"{status['overall_progress']:.1f}%") console.print(table) # Book details book_table = Table(title="Book Progress") book_table.add_column("Book", style="cyan") book_table.add_column("Status", style="white") book_table.add_column("Progress", style="green") book_table.add_column("Completed", style="green") book_table.add_column("Failed", style="red") for book in status["books"]: status_style = { "pending": "dim", "in_progress": "yellow", "completed": "green", "failed": "red", }.get(book["status"], "white") book_table.add_row( book["book"], f"[{status_style}]{book['status']}[/{status_style}]", f"{book['progress']:.1f}%", str(book["completed_verses"]), str(book["failed_verses"]), ) console.print(book_table) return 0 except ValueError as e: console.print(f"[red]Error:[/red] {e}") return 1 def jobs_list_cmd(args: argparse.Namespace) -> int: """Handle the jobs list command.""" from src.orchestration.state import JobStateStore data_dir = get_data_dir() db_path = data_dir / "jobs" / "state.db" if not db_path.exists(): console.print("[yellow]No job database found[/yellow]") return 0 state_store = JobStateStore(db_path) jobs = state_store.list_jobs(limit=args.limit) if not jobs: console.print("[yellow]No jobs found[/yellow]") return 0 table = Table(title="Translation Jobs") table.add_column("Job ID", style="cyan") table.add_column("Status", style="white") table.add_column("Books", style="green") table.add_column("Progress", style="yellow") table.add_column("Created", style="dim") for job in jobs: status_style = { "pending": "dim", "in_progress": "yellow", "completed": "green", "failed": "red", }.get(job.status.value, "white") progress = 0 if job.total_verses > 0: progress = job.completed_verses / job.total_verses * 100 table.add_row( job.job_id[:8] + "...", f"[{status_style}]{job.status.value}[/{status_style}]", str(len(job.books)), f"{progress:.1f}%", job.created_at.strftime("%Y-%m-%d %H:%M"), ) console.print(table) return 0 def jobs_clean_cmd(args: argparse.Namespace) -> int: """Handle the jobs clean command.""" from src.orchestration.state import JobStateStore data_dir = get_data_dir() db_path = data_dir / "jobs" / "state.db" if not db_path.exists(): console.print("[yellow]No job database found[/yellow]") return 0 state_store = JobStateStore(db_path) # Delete specific job if args.job_id: if state_store.delete_job(args.job_id): console.print(f"[green]Deleted job:[/green] {args.job_id}") else: console.print(f"[red]Job not found:[/red] {args.job_id}") return 1 return 0 # Delete old jobs if args.older_than_days: count = state_store.delete_jobs_older_than(args.older_than_days) console.print(f"[green]Deleted {count} jobs older than {args.older_than_days} days[/green]") return 0 # Delete completed jobs (keep latest N) keep = args.keep_latest if args.keep_latest is not None else 1 count = state_store.delete_completed_jobs(keep_latest=keep) console.print(f"[green]Deleted {count} completed jobs (kept {keep} latest)[/green]") return 0 def jobs_stats_cmd(args: argparse.Namespace) -> int: """Handle the jobs stats command.""" from src.orchestration.state import JobStateStore data_dir = get_data_dir() db_path = data_dir / "jobs" / "state.db" if not db_path.exists(): console.print("[yellow]No job database found[/yellow]") return 0 state_store = JobStateStore(db_path) stats = state_store.get_database_stats() table = Table(title="Job Database Statistics") table.add_column("Metric", style="cyan") table.add_column("Value", style="green") table.add_row("Total Jobs", str(stats["total_jobs"])) table.add_row("Pending Jobs", str(stats["pending_jobs"])) table.add_row("In Progress Jobs", str(stats["in_progress_jobs"])) table.add_row("Completed Jobs", str(stats["completed_jobs"])) table.add_row("Failed Jobs", str(stats["failed_jobs"])) table.add_row("Total Verse Records", str(stats["total_verse_records"])) table.add_row("Database Size", f"{stats['database_size_mb']:.2f} MB") console.print(table) return 0 def main() -> int: """Main entry point.""" parser = argparse.ArgumentParser( description="Japanese Bible Translator - Machine-generated modern Japanese translation" ) subparsers = parser.add_subparsers(dest="command", help="Available commands") originals_parser = subparsers.add_parser("build-originals", help="Build original-language parallel assets offline") originals_parser.add_argument("--out", type=Path) alignment_parser = subparsers.add_parser("audit-alignment", help="Build the reviewed numbering candidate and unresolved queue offline") alignment_parser.add_argument("--source", type=Path, default=get_data_dir() / "output") alignment_parser.add_argument("--review", type=Path, default=Path(__file__).parent / "reports/nichijo-v1-numbering-review.json") alignment_parser.add_argument("--out", type=Path, default=Path(__file__).parent / "reports/nichijo-v1-numbering-alignment.json") aligned_export_parser = subparsers.add_parser("export-aligned", help="Export a complete reviewed 日常訳第1版 release offline") aligned_export_parser.add_argument("--source", type=Path, default=get_data_dir() / "output") aligned_export_parser.add_argument("--audit", type=Path, default=Path(__file__).parent / "reports/nichijo-v1-numbering-alignment.json") aligned_export_parser.add_argument("--out", type=Path, required=True) repair_import_parser = subparsers.add_parser("import-alignment-repairs", help="Import a verified repair evidence bundle offline") repair_import_parser.add_argument("--review", type=Path, required=True) repair_import_parser.add_argument("--bundle", type=Path, required=True) repair_import_parser.add_argument("--out", type=Path, required=True) # Verse translation verse_parser = subparsers.add_parser("verse", help="Translate a single verse") verse_parser.add_argument("verse", help="Verse reference (e.g., 'john:1:1')") verse_parser.add_argument( "-f", "--force", action="store_true", help="Re-translate even if exists" ) verse_parser.add_argument( "-v", "--validate", action="store_true", help="Validate the translation" ) verse_parser.add_argument( "--no-evaluate", action="store_true", help="Disable automatic evaluation" ) # Chapter translation chapter_parser = subparsers.add_parser("chapter", help="Translate a chapter") chapter_parser.add_argument("book", help="Book name (e.g., 'john')") chapter_parser.add_argument("chapter", type=int, help="Chapter number") chapter_parser.add_argument( "-f", "--force", action="store_true", help="Re-translate existing" ) chapter_parser.add_argument( "--no-evaluate", action="store_true", help="Disable automatic evaluation" ) # Book translation book_parser = subparsers.add_parser("book", help="Translate an entire book") book_parser.add_argument("book", help="Book name (e.g., 'john')") book_parser.add_argument( "-f", "--force", action="store_true", help="Re-translate existing" ) book_parser.add_argument( "--no-evaluate", action="store_true", help="Disable automatic evaluation" ) # Export export_parser = subparsers.add_parser("export", help="Export translations") export_parser.add_argument("book", help="Book name to export") export_parser.add_argument( "formats", nargs="+", choices=["json", "text", "html"], help="Output formats" ) export_parser.add_argument("-o", "--output", help="Output directory") # Status status_parser = subparsers.add_parser("status", help="Show translation status") status_parser.add_argument("book", help="Book name") # Evaluate existing translations evaluate_parser = subparsers.add_parser( "evaluate", help="Evaluate existing translations" ) evaluate_parser.add_argument("book", help="Book name") evaluate_parser.add_argument("--chapter", type=int, required=True, help="Chapter") # Style configuration style_parser = subparsers.add_parser("style", help="Manage translation style") style_subparsers = style_parser.add_subparsers(dest="style_command") style_show_parser = style_subparsers.add_parser("show", help="Show current style") style_edit_parser = style_subparsers.add_parser("edit", help="Edit style settings") style_edit_parser.add_argument( "--equivalence", choices=["formal", "dynamic", "optimal"], help="Equivalence approach", ) style_edit_parser.add_argument( "--gender", choices=["traditional", "moderate_inclusive", "fully_inclusive"], help="Gender language", ) style_edit_parser.add_argument( "--readability", choices=["scholarly", "general_public", "web_native", "children"], help="Readability level", ) style_edit_parser.add_argument( "--tradition", choices=["evangelical", "catholic", "orthodox", "ecumenical"], help="Theological tradition", ) style_edit_parser.add_argument( "--consistency", choices=["strict", "balanced", "contextual"], help="Term consistency", ) style_edit_parser.add_argument( "--formality", choices=["casual", "standard", "polite", "formal"], help="Formality level", ) # Translation Memory status tm_parser = subparsers.add_parser("tm", help="Translation Memory management") tm_subparsers = tm_parser.add_subparsers(dest="tm_command") tm_status_parser = tm_subparsers.add_parser( "status", help="Show TM statistics" ) # Pericope commands pericope_parser = subparsers.add_parser( "pericope", help="Pericope-based (chunk) translation" ) pericope_subparsers = pericope_parser.add_subparsers(dest="pericope_command") # pericope list pericope_list_parser = pericope_subparsers.add_parser( "list", help="List pericopes for a book" ) pericope_list_parser.add_argument("book", help="Book name (e.g., 'john')") # pericope translate pericope_translate_parser = pericope_subparsers.add_parser( "translate", help="Translate a specific pericope" ) pericope_translate_parser.add_argument( "pericope_id", help="Pericope ID (e.g., 'john-1-prologue')" ) pericope_translate_parser.add_argument( "-f", "--force", action="store_true", help="Re-translate even if exists" ) # pericope chapter pericope_chapter_parser = pericope_subparsers.add_parser( "chapter", help="Translate a chapter by pericopes" ) pericope_chapter_parser.add_argument("book", help="Book name (e.g., 'john')") pericope_chapter_parser.add_argument("chapter", type=int, help="Chapter number") pericope_chapter_parser.add_argument( "-f", "--force", action="store_true", help="Re-translate existing" ) # pericope stats pericope_stats_parser = pericope_subparsers.add_parser( "stats", help="Show pericope data statistics" ) # TUI tui_parser = subparsers.add_parser("tui", help="Launch the TUI viewer") tui_parser.add_argument("-d", "--data-dir", help="Data directory (default: data)") # Full Bible translation bible_parser = subparsers.add_parser( "bible", help="Translate full Bible with parallel processing" ) bible_parser.add_argument( "--books", nargs="+", help="Specific books to translate (default: all 66)" ) bible_parser.add_argument( "--nt-only", action="store_true", help="Only translate New Testament (27 books)" ) bible_parser.add_argument( "--ot-only", action="store_true", help="Only translate Old Testament (39 books)" ) bible_parser.add_argument( "-p", "--parallel", type=int, default=4, help="Number of parallel book workers (default: 4)" ) bible_parser.add_argument( "--dry-run", action="store_true", help="Show what would be translated" ) # Resume incomplete job resume_parser = subparsers.add_parser( "resume", help="Resume an incomplete translation job" ) resume_parser.add_argument( "--job-id", help="Specific job ID to resume (default: latest incomplete)" ) resume_parser.add_argument( "-p", "--parallel", type=int, default=4, help="Number of parallel book workers (default: 4)" ) # Job status job_status_parser = subparsers.add_parser( "job-status", help="Show status of a translation job" ) job_status_parser.add_argument("job_id", help="Job ID to check") # Jobs management jobs_parser = subparsers.add_parser( "jobs", help="Manage translation jobs and database cleanup" ) jobs_subparsers = jobs_parser.add_subparsers(dest="jobs_command") # jobs list jobs_list_parser = jobs_subparsers.add_parser( "list", help="List all translation jobs" ) jobs_list_parser.add_argument( "-n", "--limit", type=int, default=20, help="Maximum number of jobs to show (default: 20)" ) # jobs clean jobs_clean_parser = jobs_subparsers.add_parser( "clean", help="Clean old jobs from database" ) jobs_clean_parser.add_argument( "--job-id", help="Delete a specific job by ID" ) jobs_clean_parser.add_argument( "--older-than-days", type=int, help="Delete jobs older than this many days" ) jobs_clean_parser.add_argument( "--keep-latest", type=int, help="Keep the N most recent completed jobs (default: 1)" ) # jobs stats jobs_stats_parser = jobs_subparsers.add_parser( "stats", help="Show database statistics" ) args = parser.parse_args() if args.command == "build-originals": from src.output.originals import WEB, build report = build(args.out or WEB / "public/originals") console.print(f"Built {len(report['files'])} original-language books") return 0 elif args.command == "audit-alignment": from src.output.alignment import build_audit report = build_audit(args.source, args.review, args.out) console.print(report['summary']) return 0 if report['numbering_ready'] else 1 elif args.command == "export-aligned": from src.output.alignment import export_aligned console.print(export_aligned(args.source, args.audit, args.out)) return 0 elif args.command == "import-alignment-repairs": from src.output.repair_review import import_bundle count = import_bundle(args.review, args.bundle, args.out) console.print(f"Imported {count} verified target repairs") return 0 elif args.command == "verse": return translate_verse_cmd(args) elif args.command == "chapter": return translate_chapter_cmd(args) elif args.command == "book": return translate_book_cmd(args) elif args.command == "export": return export_cmd(args) elif args.command == "status": return status_cmd(args) elif args.command == "evaluate": return evaluate_cmd(args) elif args.command == "style": if args.style_command == "show": return style_show_cmd(args) elif args.style_command == "edit": return style_edit_cmd(args) else: return style_show_cmd(args) elif args.command == "tm": if args.tm_command == "status": return tm_status_cmd(args) else: return tm_status_cmd(args) elif args.command == "pericope": if args.pericope_command == "list": return pericope_list_cmd(args) elif args.pericope_command == "translate": return pericope_translate_cmd(args) elif args.pericope_command == "chapter": return pericope_chapter_cmd(args) elif args.pericope_command == "stats": return pericope_stats_cmd(args) else: return pericope_stats_cmd(args) elif args.command == "tui": return tui_cmd(args) elif args.command == "bible": return bible_cmd(args) elif args.command == "resume": return resume_cmd(args) elif args.command == "job-status": return job_status_cmd(args) elif args.command == "jobs": if args.jobs_command == "list": return jobs_list_cmd(args) elif args.jobs_command == "clean": return jobs_clean_cmd(args) elif args.jobs_command == "stats": return jobs_stats_cmd(args) else: return jobs_list_cmd(args) else: parser.print_help() return 0 if __name__ == "__main__": sys.exit(main())