Spaces:
Paused
Paused
| #!/usr/bin/env python3 | |
| """Translation pipeline verification tool. | |
| Runs the full translation pipeline on a JSON file and prints a detailed | |
| summary of segments found, translations applied, length violations, and | |
| glossary terms — useful for manual debugging and smoke-testing. | |
| Usage: | |
| python test/verify_translate.py --input test/fixtures/mini_output.json --api-key $KEY | |
| python test/verify_translate.py --input doc.json --provider gemini --api-key $KEY | |
| python test/verify_translate.py --input doc.json --no-glossary --verbose | |
| """ | |
| import argparse | |
| import json | |
| import logging | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from pdf2zh.translation import ( | |
| TranslatorConfig, | |
| collect_translatables, | |
| segments_to_chunks, | |
| translate_document, | |
| ) | |
| from pdf2zh.translation.config import PROVIDERS | |
| logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") | |
| logger = logging.getLogger(__name__) | |
| def verify_translation( | |
| input_path: str, | |
| output_path: str | None, | |
| cfg: TranslatorConfig, | |
| ) -> None: | |
| path = Path(input_path) | |
| if not path.exists(): | |
| raise FileNotFoundError(f"Input JSON not found: {path}") | |
| with open(path, encoding="utf-8") as f: | |
| doc = json.load(f) | |
| tasks = collect_translatables(doc) | |
| chunks = segments_to_chunks(tasks, cfg.chunk_bytes) | |
| logger.info(f"Translating {path} ({len(tasks)} segments, {len(chunks)} chunks)...") | |
| out = translate_document(doc, cfg) | |
| out_path = ( | |
| Path(output_path) if output_path else path.with_suffix(".translated.json") | |
| ) | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(out_path, "w", encoding="utf-8") as f: | |
| json.dump(out, f, ensure_ascii=False, indent=2) | |
| # Collect stats after translation | |
| translated_count = 0 | |
| untranslated: list[dict] = [] | |
| violations: list[dict] = [] | |
| for task in tasks: | |
| result = task.target.get(task.write_key, "") | |
| if result and result != task.text: | |
| translated_count += 1 | |
| if len(task.text) >= 20: | |
| ratio = abs(len(result) - len(task.text)) / max(len(task.text), 1) | |
| if ratio > cfg.length_tolerance: | |
| violations.append( | |
| { | |
| "id": task.id, | |
| "src_len": len(task.text), | |
| "out_len": len(result), | |
| "ratio": f"{ratio:.1%}", | |
| "src": task.text[:60], | |
| "out": result[:60], | |
| } | |
| ) | |
| else: | |
| untranslated.append({"id": task.id, "text": task.text[:60]}) | |
| print("\nTranslation Summary") | |
| print("=" * 70) | |
| print(f"Input: {path}") | |
| print(f"Output: {out_path}") | |
| print(f"Provider: {cfg.provider} model={cfg.model}") | |
| print(f"Segments: {len(tasks)}") | |
| print(f"Chunks: {len(chunks)}") | |
| print(f"Translated: {translated_count}/{len(tasks)}") | |
| if violations: | |
| print(f"\nLength violations ({len(violations)}):") | |
| for v in violations: | |
| print( | |
| f" id={v['id']} {v['ratio']} src={v['src_len']}ch out={v['out_len']}ch" | |
| ) | |
| print(f" src: {v['src']}") | |
| print(f" out: {v['out']}") | |
| else: | |
| print("\nLength violations: none") | |
| if untranslated: | |
| print(f"\nUntranslated ({len(untranslated)}):") | |
| for u in untranslated: | |
| print(f" id={u['id']}: {u['text']}") | |
| print("\nSegment detail:") | |
| w = 45 | |
| print(f" {'ID':<4} {'FIELD':<20} {'SRC':<{w}} {'OUT':<{w}} STATUS") | |
| print(" " + "-" * (4 + 1 + 20 + 1 + w + 1 + w + 1 + 6)) | |
| for task in tasks: | |
| result = task.target.get(task.write_key, "") | |
| status = "OK " if (result and result != task.text) else "MISS" | |
| src_display = task.text[: w - 2] + ".." if len(task.text) > w else task.text | |
| out_display = result[: w - 2] + ".." if len(result) > w else result | |
| print( | |
| f" {task.id:<4} {task.write_key:<20} {src_display:<{w}} {out_display:<{w}} {status}" | |
| ) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser( | |
| description="Verify translation pipeline with detailed output", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| python test/verify_translate.py --input test/fixtures/mini_output.json --api-key $KEY | |
| python test/verify_translate.py --input doc.json --provider gemini --api-key $KEY | |
| python test/verify_translate.py --input doc.json --no-glossary --verbose | |
| """, | |
| ) | |
| parser.add_argument("--input", "-i", required=True, help="Input JSON file") | |
| parser.add_argument("--output", "-o", default=None, help="Output JSON file") | |
| parser.add_argument("--src", dest="source_language", default="") | |
| parser.add_argument("--tgt", dest="target_language", default="") | |
| parser.add_argument("--provider", default="openrouter", choices=list(PROVIDERS)) | |
| parser.add_argument("--model", default=None) | |
| parser.add_argument("--api-key", dest="api_key", default=None) | |
| parser.add_argument("--concurrent", type=int, default=5) | |
| parser.add_argument("--chunk-bytes", type=int, default=3000) | |
| parser.add_argument("--no-glossary", action="store_true") | |
| parser.add_argument("--length-tolerance", type=float, default=0.15) | |
| parser.add_argument("--verbose", action="store_true") | |
| args = parser.parse_args() | |
| if args.verbose: | |
| logging.getLogger().setLevel(logging.DEBUG) | |
| cfg = TranslatorConfig( | |
| source_language=args.source_language, | |
| target_language=args.target_language, | |
| provider=args.provider, | |
| model=args.model, | |
| api_key=args.api_key, | |
| concurrent=args.concurrent, | |
| chunk_bytes=args.chunk_bytes, | |
| glossary_enabled=not args.no_glossary, | |
| length_tolerance=args.length_tolerance, | |
| ) | |
| try: | |
| verify_translation(args.input, args.output, cfg) | |
| except FileNotFoundError as e: | |
| logger.error(str(e)) | |
| sys.exit(1) | |
| except Exception as e: | |
| logger.exception(f"Error: {e}") | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() | |