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:
谭凯
2026-01-31 22:49:44 +08:00
parent 9ef82393be
commit 7a93c52b42
306 changed files with 30313 additions and 1071 deletions
+69
View File
@@ -0,0 +1,69 @@
import argparse
import sys
import os
from pathlib import Path
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from src.preprocessing.epub_cleaner import EpubCleaner
from src.preprocessing.text_extractor import FineGrainedExtractor
from src.translation.manifest_manager import ManifestManager
from src.common.data_model import BookStructure
from src.common.utils import setup_logger, ensure_directory
from src.common.exceptions import EpubTranslatorError
logger = setup_logger("pipeline_preprocess")
def run_preprocess(args):
input_path = Path(args.input_epub)
if not input_path.exists():
logger.error(f"Input file not found: {input_path}")
sys.exit(1)
book_name = input_path.stem
work_root = Path("work") / book_name
ensure_directory(work_root)
structure_path = work_root / "book_structure.json"
manifest_path = work_root / "manifest.json"
try:
# 1. Clean / Load Structure
if structure_path.exists() and not args.force:
logger.info(f"Reusing existing structure: {structure_path}")
structure = BookStructure.load(structure_path)
else:
logger.info("Cleaning EPUB...")
cleaner = EpubCleaner(input_path, work_root)
structure_path = cleaner.clean()
structure = BookStructure.load(structure_path)
# 2. Extract Text
logger.info("Extracting text segments...")
extractor = FineGrainedExtractor()
entries = extractor.extract(structure)
# 3. Update Manifest
logger.info(f"Updating manifest: {manifest_path}")
manager = ManifestManager(manifest_path)
manager.load()
manager.add_entries(entries)
manager.save()
logger.info("Preprocessing complete.")
except EpubTranslatorError as e:
logger.error(f"Preprocessing failed: {e}")
sys.exit(1)
except Exception as e:
logger.critical(f"Unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Step 1: Preprocessing (Clean + Extract)")
parser.add_argument("input_epub", help="Path to input EPUB")
parser.add_argument("--force", action="store_true", help="Force re-clean")
args = parser.parse_args()
run_preprocess(args)
+91
View File
@@ -0,0 +1,91 @@
import argparse
import sys
import os
import asyncio
from pathlib import Path
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from src.translation.translator_engine import Translator
from src.translation.llm_client import LLMClient
from src.translation.manifest_manager import ManifestManager
from src.preprocessing.profiler import BookProfiler
from src.common.utils import setup_logger
from src.common.exceptions import EpubTranslatorError
from src.common.config import load_global_config
logger = setup_logger("pipeline_translate")
async def run_translate(args):
# Resolve paths
if args.book_name:
book_name = args.book_name
elif args.input_epub:
book_name = Path(args.input_epub).stem
else:
logger.error("Must provide --book-name or --input-epub")
sys.exit(1)
work_root = Path("work") / book_name
manifest_path = work_root / "manifest.json"
if not manifest_path.exists():
logger.error(f"Manifest not found: {manifest_path}. Run Step 1 first.")
sys.exit(1)
# Load Config
config = load_global_config()
llm_conf = config.get("llm", {})
trans_conf = config.get("translation", {})
api_key = llm_conf.get("api_key")
if not api_key:
logger.error("OPENAI_API_KEY not found in env or config.")
sys.exit(1)
try:
# Load Manifest
manager = ManifestManager(manifest_path)
manager.load()
# Init components
# Allow CLI args to override config if needed (not implemented yet, taking config partial priority)
llm = LLMClient(
api_key=api_key,
base_url=llm_conf.get("base_url"),
model=args.model if args.model else llm_conf.get("model", "gpt-3.5-turbo"),
requests_per_minute=llm_conf.get("requests_per_minute", 60),
concurrent_requests=llm_conf.get("concurrent_requests", 5)
)
# Profile
profiler = BookProfiler(llm)
profile = await profiler.analyze(manager.entries)
logger.info(f"Book Profile: {profile.title} ({profile.genre})")
# Translate
target_chunk_size = trans_conf.get("chunk_size", 4000)
translator = Translator(llm, chunk_size=target_chunk_size)
await translator.translate(manager.entries, profile)
# Save final state
manager.save()
await llm.close()
logger.info("Translation complete.")
except EpubTranslatorError as e:
logger.error(f"Translation failed: {e}")
sys.exit(1)
except Exception as e:
logger.critical(f"Unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Step 2: Translation")
parser.add_argument("--input-epub", help="Path to original EPUB (to derive book name)")
parser.add_argument("--book-name", help="Book name (folder name in work/)")
parser.add_argument("--model", default=None, help="LLM Model (overrides config)")
args = parser.parse_args()
asyncio.run(run_translate(args))
+72
View File
@@ -0,0 +1,72 @@
import argparse
import sys
import os
from pathlib import Path
# Add project root to sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from src.assembly.backfiller import BackfillEngine
from src.assembly.builder import BilingualBuilder
from src.translation.manifest_manager import ManifestManager
from src.common.data_model import BookStructure
from src.common.utils import setup_logger, ensure_directory
from src.common.exceptions import EpubTranslatorError
logger = setup_logger("pipeline_assemble")
def run_assemble(args):
input_path = Path(args.input_epub)
if not input_path.exists():
logger.error(f"Input file not found: {input_path}")
sys.exit(1)
book_name = input_path.stem
work_root = Path("work") / book_name
structure_path = work_root / "book_structure.json"
manifest_path = work_root / "manifest.json"
if not structure_path.exists() or not manifest_path.exists():
logger.error("Missing structure or manifest. Run Step 1.")
sys.exit(1)
output_dir = Path(args.output_dir)
ensure_directory(output_dir)
try:
# Load Data
structure = BookStructure.load(structure_path)
manager = ManifestManager(manifest_path)
manager.load()
# Backfill
logger.info(f"Backfilling translations (Mode: {args.mode})...")
backfiller = BackfillEngine()
updated_structure = backfiller.backfill(structure, manager.entries, mode=args.mode)
# Build
logger.info("Building EPUB...")
builder = BilingualBuilder(work_root, original_epub_path=input_path)
output_filename = f"{book_name}_{args.mode}.epub"
output_path = output_dir / output_filename
builder.build(updated_structure, output_path)
logger.info(f"Assembly complete. Output: {output_path}")
except EpubTranslatorError as e:
logger.error(f"Assembly failed: {e}")
sys.exit(1)
except Exception as e:
logger.critical(f"Unexpected error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Step 3: Assembly (Backfill + Build)")
parser.add_argument("input_epub", help="Path to original EPUB")
parser.add_argument("--output-dir", default="output", help="Output directory")
parser.add_argument("--mode", default="bilingual", choices=["bilingual", "target_only"], help="Output mode")
args = parser.parse_args()
run_assemble(args)