feat: Release v0.10 - Modular Architecture & External Config
- Refactor codebase into src/ (preprocessing, translation, assembly) - Add pipeline/ scripts for individual stages - Externalize configuration to config/config.yaml - Fix Cover Image preservation - Update documentation and manuals
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user