PDFTranslator / test /verify_translate.py
hoang.nguyen6
deploy
f66643d unverified
Raw
History Blame Contribute Delete
6.27 kB
#!/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()