import argparse import sys import os import asyncio from pathlib import Path from dotenv import load_dotenv from src.epub_cleaner import EpubCleaner from src.book_profiler import BookProfiler from src.fine_grained_extractor import FineGrainedExtractor from src.manifest_manager import ManifestManager from src.translator import Translator from src.llm_client import LLMClient from src.backfill_engine import BackfillEngine from src.bilingual_builder import BilingualBuilder from src.data_model import BookStructure from src.utils import setup_logger, ensure_directory from src.exceptions import EpubTranslatorError logger = setup_logger("main") # Unified work directory structure # .work/ # ├── {book_name}/ # │ ├── book_structure.json # │ ├── manifest.json # │ ├── assets/ # │ └── chunks/ def get_work_dirs(input_path: Path) -> dict: """Get work directory paths for a specific book.""" book_name = input_path.stem work_root = Path(".work") / book_name return { "root": work_root, "structure": work_root / "book_structure.json", "manifest": work_root / "manifest.json", "assets": work_root / "assets", "chunks": work_root / "chunks", } async def run_pipeline(args): input_path = Path(args.input_epub) output_dir = Path(args.output_dir) # Get work directories for this book work = get_work_dirs(input_path) ensure_directory(work["root"]) ensure_directory(output_dir) # Load Environment load_dotenv() api_key = os.getenv("OPENAI_API_KEY") base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") if not api_key: logger.warning("OPENAI_API_KEY not found in .env. LLM features may fail.") try: # 1. Preprocessing - Reuse book_structure.json if exists if work["structure"].exists() and not args.force_clean: logger.info(f"Reusing existing book_structure: {work['structure']}") structure = BookStructure.load(work["structure"]) else: logger.info("Cleaning EPUB and generating book_structure...") cleaner = EpubCleaner(input_path, work["root"]) book_structure_json = cleaner.clean() structure = BookStructure.load(book_structure_json) # 2. Extraction extractor = FineGrainedExtractor() manifest_entries = extractor.extract(structure) # 3. Manifest Management manifest_manager = ManifestManager(work["manifest"]) manifest_manager.load() # Load existing if any manifest_manager.add_entries(manifest_entries) manifest_manager.save() # 4. Translation if not args.skip_translation: if not api_key: logger.error("Cannot translate without API Key. Use --skip-translation to test pipeline.") sys.exit(1) llm_client = LLMClient(api_key=api_key, base_url=base_url, model=args.model) # Profiling profiler = BookProfiler(llm_client) profile = await profiler.analyze(manifest_manager.entries) logger.info(f"Book Profile: {profile}") # Translation translator = Translator(llm_client) await translator.translate(manifest_manager.entries, profile) manifest_manager.save() await llm_client.close() else: logger.info("Skipping translation step.") # 5. Backfill backfiller = BackfillEngine() updated_structure = backfiller.backfill(structure, manifest_manager.entries, mode=args.mode) # 6. Assembly - Pass original EPUB for TOC preservation builder = BilingualBuilder(work["root"], original_epub_path=input_path) output_filename = f"bilingual_{input_path.name}" output_path = output_dir / output_filename created_epub = builder.build(updated_structure, output_path) logger.info(f"Pipeline completed! Output: {created_epub}") except EpubTranslatorError as e: logger.error(f"An error occurred: {e}") sys.exit(1) except Exception as e: logger.critical(f"Unexpected error: {e}") import traceback traceback.print_exc() sys.exit(1) def main(): parser = argparse.ArgumentParser(description="EPUB Bilingual Translator") parser.add_argument("input_epub", help="Path to the input EPUB file") parser.add_argument("--output-dir", default="output", help="Directory for output files") parser.add_argument("--model", default="gpt-3.5-turbo", help="LLM Model to use") parser.add_argument("--mode", default="bilingual", choices=["bilingual", "target_only"], help="Output mode") parser.add_argument("--skip-translation", action="store_true", help="Skip LLM translation (for testing)") parser.add_argument("--force-clean", action="store_true", help="Force re-clean EPUB even if book_structure exists") args = parser.parse_args() asyncio.run(run_pipeline(args)) if __name__ == "__main__": main()