"""Prompt construction for Bible translation.""" from __future__ import annotations from typing import TYPE_CHECKING import os from src.context.glossary import GlossaryManager from src.models.pericope import ChunkTranslationContext from src.models.style import TranslationStyle, build_system_prompt from src.models.verse import TranslationContext, Verse, VerseReference from src.pipeline.morphology import select_morphology_notes # Morphology exposure measured as quality-neutral in two A/Bs (famous and # hard-passage sets) — default off to save prompt tokens; enable to experiment. MORPHOLOGY_ENABLED = os.environ.get("TRANSLATE_BIBLE_MORPH", "0") == "1" def _morphology_notes_for(verse: Verse, limit: int = 6) -> list[str]: """Morphology notes for a verse, respecting the feature flag.""" if not MORPHOLOGY_ENABLED: return [] if verse.greek_words: return select_morphology_notes(verse.greek_words, "greek", limit=limit) if verse.hebrew_words: return select_morphology_notes(verse.hebrew_words, "hebrew", limit=limit) return [] if TYPE_CHECKING: from src.context.translation_memory import TranslationMemoryManager from src.models.translation_memory import TMMatch # Default system prompt (for backward compatibility) SYSTEM_PROMPT = """You are an expert Bible translator creating a modern Japanese translation. === TRANSLATION PHILOSOPHY === EQUIVALENCE APPROACH: Dynamic/Functional - Prioritize thought-for-thought translation over word-for-word literalism - Ask "how would a native Japanese speaker naturally express this idea?" - Preserve the meaning and impact, not necessarily the exact syntax - Restructure sentences for natural Japanese flow when needed GENDER LANGUAGE: Moderate Inclusive - Use gender-inclusive language for clearly generic references - ἀδελφοί (adelphoi): render as "兄弟姉妹" when context indicates mixed group - ἄνθρωπος (anthropos): use "人" or "人々" for generic humanity, not just "男" - Preserve gender-specific language where the original clearly indicates it READABILITY: Web-native (young adults, online-media register) - Target readers in their 20s whose daily reading is online media - Short sentences: one idea per sentence - Everyday vocabulary only; prefer 和語 over stiff literary 漢語 - Keep every concrete content word — modernize the wording, never the content THEOLOGICAL TRADITION: Protestant Evangelical - Follow Protestant canon and interpretive tradition - Translate key theological terms in line with evangelical understanding - Examples: δικαιοσύνη as "義" (righteousness), σωτηρία as "救い" (salvation) TEXTUAL BASIS: MorphGNT/SBLGNT - Follow the provided source text without incorporating textual variants - Do not emend or correct based on other manuscript traditions TERM CONSISTENCY: Balanced - Maintain consistent translation for key theological terms (provided in glossary) - Allow contextual variation for common words where meaning shifts - Prioritize natural reading over rigid concordance for non-technical vocabulary === TARGET AUDIENCE === The translation should be accessible to young adult Japanese nonbelievers who are not familiar with Christianity. - Use vocabulary and expressions that require no prior church background - Explain concepts through natural phrasing rather than relying on traditional Christian terminology - Avoid insider language that would confuse those unfamiliar with the faith - HOWEVER: No compromise should be made in conveying the true Biblical meaning of the text - Accuracy to the original meaning takes absolute priority over accessibility - When tension exists between accessibility and faithfulness, choose faithfulness === TARGET STYLE === Modern natural Japanese (現代のウェブ記事・ナレーションのような平易な日本語) - Use contemporary vocabulary and grammar - Avoid archaic expressions (なり、たり、候) - Narrative, exposition, and poetry: plain form (である体/だ体), as in 新共同訳 and 口語訳 - Polite form (です/ます) ONLY inside quoted speech, when natural for the speaker - Keep the same register consistently across every verse of a passage - Orthography: first-person singular is わたし (hiragana, not 私); keep orthography consistent - Prioritize clarity and accessibility === LITERARY QUALITY (by genre) === Match the literary character of the source genre: - Hebrew poetry: preserve parallel line structure, concrete imagery, quiet rhythm - Narrative: clear flowing prose; preserve deliberate repetition and refrains - Epistles: keep logical connectives and argument flow; no fragmenting of one thought - Sayings/beatitudes: preserve the parallel formula across the whole series === VERSE COHESION === A verse is often part of a longer sentence. Use the preceding and following verses in CONTEXT to make your translation read as a smooth continuation of the passage. === INPUT FORMAT === You will receive: 1. Source text (Greek for NT, Hebrew for OT) 2. Reference translations (historical Japanese and English) 3. Surrounding context (preceding and following verses) 4. Glossary terms to use consistently === OUTPUT CONTRACT (strict) === Output ONLY the final Japanese translation of the target verse. - No explanations, notes, romanization, or commentary - No alternative renderings, no self-corrections, no English of any kind - Decide on your final wording BEFORE writing; output exactly one rendering""" class PromptBuilder: """Builds prompts for translation.""" def __init__( self, glossary: GlossaryManager, style: TranslationStyle | None = None, translation_memory: TranslationMemoryManager | None = None, ) -> None: """Initialize the prompt builder. Args: glossary: Glossary manager for term formatting. style: Translation style configuration. If None, uses default. translation_memory: Translation memory manager for TM matches. """ self.glossary = glossary self._style = style self._translation_memory = translation_memory self._system_prompt: str | None = None @property def style(self) -> TranslationStyle: """Get the current translation style.""" if self._style is None: self._style = TranslationStyle() return self._style @style.setter def style(self, value: TranslationStyle) -> None: """Set the translation style and invalidate cached prompt.""" self._style = value self._system_prompt = None def build_prompt( self, context: TranslationContext, tm_matches: list[TMMatch] | None = None, ) -> str: """Build a translation prompt from context. Args: context: Full translation context. tm_matches: Optional list of translation memory matches. Returns: Formatted prompt string for the LLM. """ parts = [] # Source texts section parts.append(self._format_source_texts(context.target_verse)) # Selective morphology (key verb forms) morph_notes = _morphology_notes_for(context.target_verse) if morph_notes: parts.append( "\nMORPHOLOGY (parsing of key verb forms — use to disambiguate" " tense/aspect, voice, and participles):\n" + "\n".join(f" {n}" for n in morph_notes) ) # Reference translations section parts.append(self._format_reference_translations(context.target_verse)) # Context section parts.append(self._format_context(context)) # Translation Memory section (if matches provided) if tm_matches: parts.append(self._format_tm_matches(tm_matches)) # Glossary section if context.glossary_terms: parts.append(self._format_glossary(context)) # Translation instruction ref = context.target_verse.reference parts.append(f"\nTRANSLATE: {ref.book.title()} {ref.chapter}:{ref.verse}") parts.append("Provide only the Japanese translation:") return "\n".join(parts) def _format_tm_matches(self, matches: list[TMMatch]) -> str: """Format translation memory matches for prompt. Args: matches: List of TM matches. Returns: Formatted string for prompt inclusion. """ if not matches: return "" lines = ["\nTRANSLATION MEMORY (similar past translations for reference):"] for match in matches[:3]: # Limit to top 3 similarity_pct = int(match.similarity * 100) seg = match.segment lines.append(f" [{similarity_pct}%] {seg.reference}: {seg.target_text}") return "\n".join(lines) def _format_source_texts(self, verse: Verse) -> str: """Format source texts section.""" lines = ["SOURCE TEXT:"] if verse.greek_text: lines.append(f"Greek: {verse.greek_text}") if verse.greek_translit: lines.append(f"Romanization: {verse.greek_translit}") elif verse.hebrew_text: lines.append(f"Hebrew: {verse.hebrew_text}") if verse.hebrew_translit: lines.append(f"Romanization: {verse.hebrew_translit}") return "\n".join(lines) def _format_reference_translations(self, verse: Verse) -> str: """Format reference translations section.""" lines = ["\nREFERENCE TRANSLATIONS:"] if verse.japanese_meiji: lines.append(f"- 明治元訳 (1887): {verse.japanese_meiji}") if verse.japanese_taisho: lines.append(f"- 大正改訳 (1917): {verse.japanese_taisho}") if verse.japanese_kougo: lines.append(f"- 口語訳 (1955): {verse.japanese_kougo}") if verse.english_kjv: lines.append(f"- KJV: {verse.english_kjv}") if verse.english_asv: lines.append(f"- ASV: {verse.english_asv}") return "\n".join(lines) def _format_context(self, context: TranslationContext) -> str: """Format context section.""" lines = ["\nCONTEXT:"] # Book context if context.book_metadata: meta = context.book_metadata lines.append( f"Book: {meta.name_english} ({meta.name_japanese}) - {meta.description}" ) if meta.themes: lines.append(f"Themes: {', '.join(meta.themes)}") # Chapter summary if context.chapter_summary: lines.append(f"\nChapter Summary: {context.chapter_summary.summary}") # Preceding verses if context.preceding_verses: lines.append("\nPreceding verses:") for v in context.preceding_verses: text = self._get_best_text(v) lines.append(f" {v.reference.verse}: {text}") # Following verses if context.following_verses: lines.append("\nFollowing verses:") for v in context.following_verses: text = self._get_best_text(v) lines.append(f" {v.reference.verse}: {text}") return "\n".join(lines) def _format_glossary(self, context: TranslationContext) -> str: """Format glossary section.""" formatted = self.glossary.format_for_prompt(context.glossary_terms) return f"\nGLOSSARY (use these terms consistently):\n{formatted}" def _get_best_text(self, verse: Verse) -> str: """Get the best available text representation for context.""" # Prefer English for context (easier for the model to understand) if verse.english_kjv: return verse.english_kjv if verse.english_asv: return verse.english_asv if verse.japanese_kougo: return verse.japanese_kougo if verse.japanese_taisho: return verse.japanese_taisho if verse.japanese_meiji: return verse.japanese_meiji if verse.greek_text: return verse.greek_text if verse.hebrew_text: return verse.hebrew_text return "[text not available]" def get_system_prompt(self) -> str: """Get the system prompt for translation. Returns system prompt based on the configured style. Uses cached version if style hasn't changed. Returns: System prompt string. """ if self._style is None: return SYSTEM_PROMPT if self._system_prompt is None: self._system_prompt = build_system_prompt(self._style) return self._system_prompt def build_system_prompt(self) -> str: """Build system prompt from current style configuration. This always regenerates the prompt (no caching). Returns: System prompt string. """ return build_system_prompt(self.style) def build_chunk_prompt( self, context: ChunkTranslationContext, tm_matches: list[TMMatch] | None = None, preceding_translations: list[tuple[VerseReference, str]] | None = None, ) -> str: """Build a chunk/pericope translation prompt. Args: context: Full chunk translation context. tm_matches: Optional list of translation memory matches. preceding_translations: This project's own translations of the immediately preceding verses, as (reference, text) pairs — the new chunk must continue them seamlessly. Returns: Formatted prompt string for translating an entire pericope. """ parts = [] # Section header pericope = context.pericope title = pericope.title or pericope.get_display_range() parts.append(f"=== PERICOPE: {context.book.title()} {title} ===") if pericope.theme: parts.append(f"Theme: {pericope.theme}") parts.append("") # Source texts section (all verses) parts.append(self._format_chunk_source_texts(context)) # Selective morphology per verse (key verb forms) chunk_morph_lines = [] for verse in context.verses: notes = _morphology_notes_for(verse, limit=3) if notes: ref = f"{verse.reference.chapter}:{verse.reference.verse}" chunk_morph_lines.append(f" [{ref}] " + "; ".join(notes)) if chunk_morph_lines: parts.append( "\nMORPHOLOGY (parsing of key verb forms — use to disambiguate" " tense/aspect, voice, and participles):\n" + "\n".join(chunk_morph_lines) ) # Reference translations for each verse parts.append(self._format_chunk_reference_translations(context)) # Context section parts.append(self._format_chunk_context(context)) # Seam continuity: our own translation of the preceding passage if preceding_translations: seam_lines = [ "\nPRECEDING TEXT AS ALREADY TRANSLATED (your translation must" " read as a seamless continuation of this — match its register," " terminology, and flow):" ] for ref, text in preceding_translations: seam_lines.append(f" [{ref.chapter}:{ref.verse}] {text}") parts.append("\n".join(seam_lines)) # Translation Memory section (if matches provided) if tm_matches: parts.append(self._format_tm_matches(tm_matches)) # Glossary section if context.glossary_terms: parts.append(self._format_chunk_glossary(context)) # Translation instruction with JSON output format parts.append(self._build_chunk_instruction(context)) return "\n".join(parts) def _format_chunk_source_texts(self, context: ChunkTranslationContext) -> str: """Format source texts for all verses in chunk.""" lines = ["SOURCE TEXTS:"] for verse in context.verses: ref = f"{verse.reference.chapter}:{verse.reference.verse}" if verse.greek_text: lines.append(f"[{ref}] {verse.greek_text}") elif verse.hebrew_text: lines.append(f"[{ref}] {verse.hebrew_text}") return "\n".join(lines) def _format_chunk_reference_translations( self, context: ChunkTranslationContext ) -> str: """Format reference translations for each verse in chunk.""" lines = ["\nREFERENCE TRANSLATIONS:"] for verse in context.verses: ref = f"{verse.reference.chapter}:{verse.reference.verse}" lines.append(f"\n[{ref}]") if verse.japanese_meiji: lines.append(f" 明治元訳: {verse.japanese_meiji}") if verse.japanese_taisho: lines.append(f" 大正改訳: {verse.japanese_taisho}") if verse.english_kjv: lines.append(f" KJV: {verse.english_kjv}") return "\n".join(lines) def _format_chunk_context(self, context: ChunkTranslationContext) -> str: """Format context section for chunk translation.""" lines = ["\nCONTEXT:"] # Book context if context.book_metadata: meta = context.book_metadata lines.append( f"Book: {meta.name_english} ({meta.name_japanese}) - {meta.description}" ) # Chapter summary if context.chapter_summary: lines.append(f"Chapter Summary: {context.chapter_summary.summary}") # Preceding context if context.preceding_verses: lines.append("\nPreceding context:") for v in context.preceding_verses: ref = f"{v.reference.chapter}:{v.reference.verse}" text = self._get_best_text(v) lines.append(f" [{ref}] {text}") # Following context if context.following_verses: lines.append("\nFollowing context:") for v in context.following_verses: ref = f"{v.reference.chapter}:{v.reference.verse}" text = self._get_best_text(v) lines.append(f" [{ref}] {text}") return "\n".join(lines) def _format_chunk_glossary(self, context: ChunkTranslationContext) -> str: """Format glossary section for chunk translation.""" formatted = self.glossary.format_for_prompt(context.glossary_terms) return f"\nGLOSSARY (use these terms consistently):\n{formatted}" def _build_chunk_instruction(self, context: ChunkTranslationContext) -> str: """Build the translation instruction with JSON output format.""" verse_refs = context.pericope.verses verse_list = ", ".join(f'"{ref}"' for ref in verse_refs) return f''' === TRANSLATION INSTRUCTIONS === Translate all {len(verse_refs)} verses as a COHESIVE UNIT: - Read the entire pericope first to understand the flow - Ensure smooth transitions between verses - Maintain consistent terminology throughout - Preserve narrative/argument coherence OUTPUT FORMAT (JSON): Return ONLY a JSON object mapping verse references to translations. No markdown code blocks, no explanations, just the JSON object. Example format: {{ "1:1": "初めに言葉があった。...", "1:2": "この言葉は..." }} Translate verses: [{verse_list}] Respond with ONLY the JSON object:''' def get_chunk_system_prompt(self) -> str: """Get system prompt for chunk/pericope translation. Uses base style settings but adds chunk-specific instructions. Returns: System prompt string for chunk translation. """ base_prompt = self.get_system_prompt() chunk_addition = """ === CHUNK TRANSLATION MODE === You are translating a PERICOPE (cohesive text unit) rather than individual verses. Key principles for pericope translation: 1. Translate with narrative/discourse flow in mind 2. Use transitional words naturally (そして、それで、しかし、etc.) 3. Ensure pronouns have clear antecedents across verses 4. Maintain consistent verb tenses within the unit 5. Consider the entire pericope's theme when choosing vocabulary 6. If a "PRECEDING TEXT AS ALREADY TRANSLATED" section is present, your first verse must read as a seamless continuation of it — same register, same terminology, natural transition Output: JSON object with verse-aligned translations for easy verse lookup. Each verse should stand alone but flow naturally with adjacent verses.""" return base_prompt + chunk_addition