DEV Community

龚旭东
龚旭东

Posted on

How We Built '直接翻译与含义确认' — A Two‑Pass Translation Pipeline with LLMs

Using direct translation and meaning confirmation to reduce ambiguity in AI‑powered book translation.

At LectuLibre, we translate entire books using LLMs like Claude and DeepSeek. Early on, we noticed that literal or direct translations often missed the nuance of ambiguous phrases — a character’s nickname, a culturally specific idiom, or a word with multiple meanings. A single‑pass translation pipeline just wasn’t enough. That’s when we built 直接翻译与含义确认 (Direct Translation & Meaning Confirmation), a two‑stage process that first produces a draft translation, then re‑examines tricky bits with a meaning‑confirmation step. Here’s exactly how we did it, including the Python code, the trade‑offs, and the unexpected lessons.

The Problem: When Direct Translation Falls Short

Our initial pipeline was simple: split an EPUB into chunks, send each chunk to an LLM with a prompt like “Translate the following English text to Spanish”, and stitch the results. It worked for straightforward paragraphs, but books are full of traps:

  • Polysemous words: “He got the bat” could be the animal or sports equipment.
  • Proper names with cultural weight: A nickname like “Little Red” might need adaptation, not literal translation.
  • Idioms: “Break a leg” cannot be translated word‑for‑word.

We tried adding “be mindful of idioms” to the system prompt, but the model often still missed the mark because it lacked the broader context of the entire book.

Our Approach: Translate, Then Confirm Meaning

We decided to treat translation as a two‑step reasoning task:

  1. Direct Translation: Obtain a quick, literal‑ish translation of the chunk.
  2. Meaning Confirmation: Identify phrases in the original that are likely ambiguous, then ask the LLM to explain their meaning in context and either confirm the direct translation or propose a better one.

Because books can be huge, calling an LLM for every phrase would be prohibitively expensive. So we built a lightweight phrase selector that uses basic NLP heuristics to flag candidates: named entities, multi‑word expressions, and words with unusually high translation entropy (we’ll get to that).

Implementation: FastAPI Background Workers & Async LLM Calls

Our backend is FastAPI + SQLAlchemy + PostgreSQL, deployed on a VPS. Translation jobs are submitted via an API endpoint and processed by background workers using asyncio. Here’s the core pipeline.

Step 1: Direct Translation

We still translate chunk‑by‑chunk, but now with a prompt that explicitly asks for a conservative, literal translation. The output is a JSON object containing the translated text and a list of all original phrases that might be ambiguous (the model helps pre‑select them).

# direct_translation.py
import asyncio
import httpx
import json

DIRECT_TRANSLATE_PROMPT = """
Translate the following text from {source_lang} to {target_lang}.
Provide a literal translation and, in a separate list, any phrases that could have multiple interpretations.
Return JSON with keys: "translation", "ambiguous_phrases".
Text: {text}
"""

async def direct_translate(text: str, source_lang: str, target_lang: str) -> dict:
    async with httpx.AsyncClient(timeout=60.0) as client:
        response = await client.post(
            "https://api.anthropic.com/v1/messages",  # or DeepSeek endpoint
            headers={"x-api-key": settings.LLM_API_KEY, "anthropic-version": "2023-06-01"},
            json={
                "model": "claude-3-sonnet-20240229",
                "max_tokens": 4096,
                "temperature": 0.3,
                "messages": [{"role": "user", "content": DIRECT_TRANSLATE_PROMPT.format(
                    source_lang=source_lang, target_lang=target_lang, text=text
                )}]
            }
        )
        data = response.json()
        return json.loads(data["content"][0]["text"])
Enter fullscreen mode Exit fullscreen mode

We validate the JSON and catch parsing errors with a fallback retry.

Step 2: Meaning Confirmation for Ambiguous Phrases

For each phrase flagged by the model (or supplemented by our own heuristics), we make a second call. This time the prompt asks the LLM to explain the meaning in context and, if needed, propose a corrected translation.

# confirm_phrase.py
CONFIRM_PROMPT = """
Original text: {original_chunk}
Direct translation snippet: {direct_translation}

Focus on the phrase: "{phrase}"
What does this phrase mean in this context? Please explain in one sentence.
Then, if the direct translation is inaccurate or could be better, provide an improved translation for just this phrase in {target_lang}.
Respond in JSON: {{"meaning_explanation": str, "improved_translation": str or null}}
"""

async def confirm_phrase(
    phrase: str,
    original_chunk: str,
    direct_translation: str,
    target_lang: str
) -> dict:
    async with httpx.AsyncClient(timeout=30.0) as client:
        resp = await client.post(
            "https://api.deepseek.com/v1/chat/completions",
            headers={"Authorization": f"Bearer {settings.DEEPSEEK_API_KEY}"},
            json={
                "model": "deepseek-chat",
                "messages": [{"role": "user", "content": CONFIRM_PROMPT.format(
                    original_chunk=original_chunk,
                    direct_translation=direct_translation,
                    phrase=phrase,
                    target_lang=target_lang
                )}]
            }
        )
        return json.loads(resp.json()["choices"][0]["message"]["content"])
Enter fullscreen mode Exit fullscreen mode

We parallelise the confirmation calls for all ambiguous phrases in a chunk using asyncio.gather:

# worker.py
async def process_chunk(chunk: str, source_lang: str, target_lang: str, book_id: int):
    # 1. Direct translate
    result = await direct_translate(chunk, source_lang, target_lang)
    base_translation = result["translation"]
    ambiguous = result["ambiguous_phrases"]

    # Enhance with our own heuristic: e.g., proper nouns, long phrases
    extra_phrases = extract_named_entities(chunk)  # simple regex / spacy stub
    all_phrases = list(set(ambiguous + extra_phrases[:5]))  # limit to 5 extra to save cost

    # 2. Confirm all phrases concurrently
    confirm_tasks = [
        confirm_phrase(p, chunk, base_translation, target_lang)
        for p in all_phrases
    ]
    confirmations = await asyncio.gather(*confirm_tasks, return_exceptions=True)

    # 3. Replace translations where improvements were found
    final_translation = base_translation
    for phrase, confirmation in zip(all_phrases, confirmations):
        if isinstance(confirmation, Exception):
            logger.warning(f"Confirmation failed for {phrase}: {confirmation}")
            continue
        if confirmation["improved_translation"]:
            final_translation = final_translation.replace(
                phrase, confirmation["improved_translation"]  # simplistic, see caveats
            )

    # Store in DB
    await save_translated_chunk(book_id, chunk, final_translation, confirmations)
Enter fullscreen mode Exit fullscreen mode

Database Schema

We record every confirmation attempt so we can learn from them and cache results for recurring phrases across the same book.

CREATE TABLE translation_confirmation (
    id SERIAL PRIMARY KEY,
    book_id INTEGER NOT NULL,
    chunk_index INTEGER NOT NULL,
    original_phrase TEXT NOT NULL,
    direct_translation TEXT,
    confirmed_translation TEXT,
    meaning_explanation TEXT,
    improved_translation TEXT,
    is_applied BOOLEAN DEFAULT false,
    created_at TIMESTAMP DEFAULT now()
);
Enter fullscreen mode Exit fullscreen mode

Trade‑offs and Performance

Cost: With two LLM calls per chunk plus parallel confirmations, cost grew by roughly 40% per page. We mitigated this by:

  • Using a cheaper model (DeepSeek) for confirmation, keeping Claude only for the direct translation.
  • Capping ambiguous phrases to 5 per chunk.
  • Caching repeated phrases (e.g., a character’s name appears in almost every chunk; we confirm it once and reuse).

Latency: The total translation time for a 300‑page book increased from ~2 minutes to ~5 minutes (end‑to‑end, async). We considered this acceptable given the quality gain.

Accuracy: We randomly sampled 200 ambiguous phrases before and after the feature. With direct translation alone, 23% had a noticeable meaning error. After confirmation, only 6% remained, and those were often subtle cases where even a human would struggle. The improvement was most dramatic in idiomatic expressions and culturally specific items.

Unexpected Lessons

  1. Confirmation needs the full chunk context: Early on, we tried confirming phrases in isolation. The LLM often hallucinated because it didn’t see the surrounding text. Always pass the whole chunk (or at least the surrounding paragraph).

  2. Simple replacement isn’t safe: Our snippet uses str.replace() to swap in improved translations. For languages with different word orders or inflections, this can break grammar. We’re now exploring an approach where the improved phrase is injected back into the LLM with a “revise this translation” prompt.

  3. Meaning explanations are gold for human review: We added a review UI where editors see the original, the direct translation, and the meaning explanation side by side. This speeds up human proofreading by 30%, making the feature valuable even beyond automatic improvement.

  4. Model choice matters more than prompt engineering: For confirmation, we found that a smaller, instruction‑tuned model (like DeepSeek‑Chat) often gave more concise and accurate explanations than the larger model used for translation. Experiment with different models for the two stages; don’t assume the same model is optimal for both.

What’s Next?

We’re building a lightweight glossary that learns from confirmations across the entire book, so that when “Little Red” is confirmed as “Caperucita” in chapter 1, it’s used automatically for the rest of the book. We’re also experimenting with letting the LLM propose a global adaptation strategy up front (e.g., “This book uses many farming metaphors; translate them culturally”).

If you’re building a similar AI‑assisted translation pipeline, the two‑pass approach is well worth the extra complexity. Start by evaluating your model’s typical failure modes — if ambiguity is the main enemy, direct translation + meaning confirmation can dramatically improve output quality.

Have you tried confidence‑based or verification steps in your LLM pipelines? We’d love to hear about your experiences in the comments.

LectuLibre is an AI‑powered book translation service; the pipeline described here is part of our production system. Full source code isn’t public, but the architectural ideas and snippets should be enough to build your own.

Top comments (0)