"""Converters between Pydantic models and Protocol Buffer messages. This module provides bidirectional conversion between the Pydantic models used internally and the Protocol Buffer messages used for serialization and I/O. Since proto compilation requires optional dependencies, this module provides dataclass-based representations that mirror the proto structure when the compiled protos are not available. """ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime, timezone from enum import IntEnum from typing import TYPE_CHECKING from src.utils.furigana import FuriganaAnnotation if TYPE_CHECKING: from src.models.glossary import GlossaryTerm as GlossaryTermModel from src.models.glossary import TheologicalDomain as TheologicalDomainModel from src.models.style import ( EquivalenceApproach as EquivalenceApproachModel, FormalityLevel as FormalityLevelModel, GenderLanguage as GenderLanguageModel, ReadabilityLevel as ReadabilityLevelModel, TermConsistency as TermConsistencyModel, TheologicalTradition as TheologicalTraditionModel, TranslationStyle as TranslationStyleModel, ) from src.models.translation_memory import ( MatchType as MatchTypeModel, SegmentLevel as SegmentLevelModel, TMMatch as TMMatchModel, TMSegment as TMSegmentModel, ) from src.models.verse import ( BookMetadata as BookMetadataModel, ChapterSummary as ChapterSummaryModel, TranslationContext as TranslationContextModel, TranslationOutput as TranslationOutputModel, Verse as VerseModel, VerseReference as VerseReferenceModel, ) from src.orchestration.provenance import TranslationProvenance as TranslationProvenanceModel # ============================================================================= # Proto-like Enums (mirrors bible_translation.proto enums) # ============================================================================= class BiblicalTestament(IntEnum): """Biblical testament classification.""" TESTAMENT_UNSPECIFIED = 0 OLD_TESTAMENT = 1 NEW_TESTAMENT = 2 # Alias for backward compatibility Testament = BiblicalTestament class TheologicalDomain(IntEnum): """Theological domain categories for terms.""" THEOLOGICAL_DOMAIN_UNSPECIFIED = 0 CHRISTOLOGY = 1 SOTERIOLOGY = 2 ESCHATOLOGY = 3 PNEUMATOLOGY = 4 ECCLESIOLOGY = 5 HAMARTIOLOGY = 6 THEOLOGY_PROPER = 7 ANTHROPOLOGY = 8 GENERAL = 9 class EquivalenceApproach(IntEnum): """Translation equivalence approach.""" EQUIVALENCE_APPROACH_UNSPECIFIED = 0 FORMAL = 1 DYNAMIC = 2 OPTIMAL = 3 class GenderLanguage(IntEnum): """Gender-inclusive language approach.""" GENDER_LANGUAGE_UNSPECIFIED = 0 TRADITIONAL = 1 MODERATE_INCLUSIVE = 2 FULLY_INCLUSIVE = 3 class ReadabilityLevel(IntEnum): """Target audience readability level.""" READABILITY_LEVEL_UNSPECIFIED = 0 SCHOLARLY = 1 GENERAL_PUBLIC = 2 CHILDREN = 3 WEB_NATIVE = 4 class TheologicalTradition(IntEnum): """Theological interpretive tradition.""" THEOLOGICAL_TRADITION_UNSPECIFIED = 0 EVANGELICAL = 1 CATHOLIC = 2 ORTHODOX = 3 ECUMENICAL = 4 class TermConsistency(IntEnum): """Term translation consistency approach.""" TERM_CONSISTENCY_UNSPECIFIED = 0 STRICT = 1 BALANCED = 2 CONTEXTUAL = 3 class FormalityLevel(IntEnum): """Japanese formality level.""" FORMALITY_LEVEL_UNSPECIFIED = 0 CASUAL = 1 STANDARD = 2 FORMAL_LEVEL = 3 POLITE = 4 class MatchType(IntEnum): """Translation memory match type.""" MATCH_TYPE_UNSPECIFIED = 0 EXACT = 1 FUZZY = 2 PHRASE = 3 class ConfidenceLevel(IntEnum): """Confidence level for translations.""" CONFIDENCE_LEVEL_UNSPECIFIED = 0 LOW = 1 MEDIUM = 2 HIGH = 3 VERY_HIGH = 4 class SegmentLevel(IntEnum): """Level of granularity for a translation segment.""" SEGMENT_LEVEL_UNSPECIFIED = 0 VERSE = 1 PHRASE = 2 # ============================================================================= # Proto-like Message Dataclasses # ============================================================================= @dataclass class VerseReferenceProto: """Proto representation of a verse reference.""" book: str = "" chapter: int = 0 verse: int = 0 @dataclass class VerseProto: """Proto representation of a Bible verse.""" reference: VerseReferenceProto = field(default_factory=VerseReferenceProto) greek_text: str | None = None greek_translit: str | None = None hebrew_text: str | None = None hebrew_translit: str | None = None japanese_meiji: str | None = None japanese_taisho: str | None = None english_kjv: str | None = None english_asv: str | None = None @dataclass class BookMetadataProto: """Proto representation of book metadata.""" book: str = "" name_japanese: str = "" name_english: str = "" testament: Testament = Testament.TESTAMENT_UNSPECIFIED author: str = "" themes: list[str] = field(default_factory=list) description: str = "" chapter_count: int = 0 verse_count: int = 0 @dataclass class ChapterSummaryProto: """Proto representation of chapter summary.""" book: str = "" chapter: int = 0 summary: str = "" key_events: list[str] = field(default_factory=list) key_characters: list[str] = field(default_factory=list) key_themes: list[str] = field(default_factory=list) @dataclass class GlossaryTermProto: """Proto representation of a glossary term.""" original: str = "" transliteration: str = "" japanese: str = "" english: str = "" notes: str | None = None lemma: str | None = None inflected_forms: list[str] = field(default_factory=list) strongs_number: str | None = None definition: str | None = None semantic_range: list[str] = field(default_factory=list) domain: TheologicalDomain = TheologicalDomain.GENERAL usage_examples: list[str] = field(default_factory=list) frequency: int = 0 related_terms: list[str] = field(default_factory=list) avoid_translations: list[str] = field(default_factory=list) @dataclass class TranslationStyleProto: """Proto representation of translation style configuration.""" equivalence_approach: EquivalenceApproach = EquivalenceApproach.EQUIVALENCE_APPROACH_UNSPECIFIED gender_language: GenderLanguage = GenderLanguage.GENDER_LANGUAGE_UNSPECIFIED readability_level: ReadabilityLevel = ReadabilityLevel.READABILITY_LEVEL_UNSPECIFIED theological_tradition: TheologicalTradition = ( TheologicalTradition.THEOLOGICAL_TRADITION_UNSPECIFIED ) textual_basis: str = "" term_consistency: TermConsistency = TermConsistency.TERM_CONSISTENCY_UNSPECIFIED formality_level: FormalityLevel = FormalityLevel.FORMALITY_LEVEL_UNSPECIFIED avoid_archaic: list[str] = field(default_factory=list) target_style_reference: str = "" additional_instructions: str | None = None @dataclass class FuriganaAnnotationProto: """Proto representation of furigana annotation.""" start_offset: int = 0 end_offset: int = 0 base_text: str = "" reading: str = "" is_standard_reading: bool = True reading_source: str = "dictionary" @dataclass class AnnotatedWordProto: """Proto representation of an annotated word.""" surface_form: str = "" lemma: str = "" part_of_speech: str = "" reading: str = "" alternative_readings: list[str] = field(default_factory=list) start_offset: int = 0 end_offset: int = 0 @dataclass class TMSegmentProto: """Proto representation of a translation memory segment.""" id: str = "" source_text: str = "" source_normalized: str = "" target_text: str = "" reference: VerseReferenceProto = field(default_factory=VerseReferenceProto) level: SegmentLevel = SegmentLevel.VERSE created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) reuse_count: int = 0 metadata: dict = field(default_factory=dict) @dataclass class TranslationMemoryMatchProto: """Proto representation of a translation memory match.""" segment: TMSegmentProto = field(default_factory=TMSegmentProto) similarity: float = 0.0 match_type: MatchType = MatchType.MATCH_TYPE_UNSPECIFIED @dataclass class TranslationProvenanceProto: """Proto representation of translation provenance.""" source_texts: dict[str, str] = field(default_factory=dict) source_hashes: dict[str, str] = field(default_factory=dict) preceding_verses: list[str] = field(default_factory=list) following_verses: list[str] = field(default_factory=list) pericope_id: str | None = None style_config_hash: str = "" glossary_hash: str = "" prompt_version: str = "v1.0" model: str = "" system_prompt: str = "" user_prompt: str = "" tm_matches_used: list[dict] = field(default_factory=list) attempt_number: int = 1 started_at: datetime | None = None completed_at: datetime | None = None evaluation_score: float | None = None evaluation_flags: list[str] = field(default_factory=list) chapter_summary: str | None = None book_metadata: dict = field(default_factory=dict) @dataclass class TranslationResultProto: """Proto representation of translation result with furigana.""" reference: VerseReferenceProto = field(default_factory=VerseReferenceProto) text: str = "" text_with_inline_furigana: str = "" furigana_annotations: list[FuriganaAnnotationProto] = field(default_factory=list) annotated_words: list[AnnotatedWordProto] = field(default_factory=list) model: str = "" timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) prompt_version: str = "" sources_used: list[str] = field(default_factory=list) glossary_terms_applied: list[str] = field(default_factory=list) confidence: ConfidenceLevel = ConfidenceLevel.CONFIDENCE_LEVEL_UNSPECIFIED confidence_score: float = 0.0 input_hash: str = "" provenance: TranslationProvenanceProto | None = None @dataclass class TranslationPromptInputProto: """Proto representation of complete translation input.""" target_verse: VerseProto = field(default_factory=VerseProto) preceding_verses: list[VerseProto] = field(default_factory=list) following_verses: list[VerseProto] = field(default_factory=list) chapter_summary: ChapterSummaryProto | None = None book_metadata: BookMetadataProto | None = None glossary_terms: list[GlossaryTermProto] = field(default_factory=list) style: TranslationStyleProto = field(default_factory=TranslationStyleProto) tm_matches: list[TranslationMemoryMatchProto] = field(default_factory=list) system_prompt: str = "" user_prompt: str = "" model: str = "" prompt_version: str = "" request_time: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) # ============================================================================= # Converter Functions: Pydantic -> Proto # ============================================================================= def verse_reference_to_proto(ref: VerseReferenceModel) -> VerseReferenceProto: """Convert a VerseReference Pydantic model to proto representation.""" return VerseReferenceProto( book=ref.book, chapter=ref.chapter, verse=ref.verse, ) def verse_to_proto(verse: VerseModel) -> VerseProto: """Convert a Verse Pydantic model to proto representation.""" return VerseProto( reference=verse_reference_to_proto(verse.reference), greek_text=verse.greek_text, greek_translit=verse.greek_translit, hebrew_text=verse.hebrew_text, hebrew_translit=verse.hebrew_translit, japanese_meiji=verse.japanese_meiji, japanese_taisho=verse.japanese_taisho, english_kjv=verse.english_kjv, english_asv=verse.english_asv, ) def _theological_domain_to_proto(domain: TheologicalDomainModel) -> TheologicalDomain: """Convert TheologicalDomain enum to proto enum.""" domain_map = { "christology": TheologicalDomain.CHRISTOLOGY, "soteriology": TheologicalDomain.SOTERIOLOGY, "eschatology": TheologicalDomain.ESCHATOLOGY, "pneumatology": TheologicalDomain.PNEUMATOLOGY, "ecclesiology": TheologicalDomain.ECCLESIOLOGY, "hamartiology": TheologicalDomain.HAMARTIOLOGY, "theology_proper": TheologicalDomain.THEOLOGY_PROPER, "anthropology": TheologicalDomain.ANTHROPOLOGY, "general": TheologicalDomain.GENERAL, } return domain_map.get(domain.value, TheologicalDomain.GENERAL) def book_metadata_to_proto(metadata: BookMetadataModel) -> BookMetadataProto: """Convert BookMetadata Pydantic model to proto representation.""" testament = Testament.NEW_TESTAMENT if metadata.testament == "NT" else Testament.OLD_TESTAMENT return BookMetadataProto( book=metadata.book, name_japanese=metadata.name_japanese, name_english=metadata.name_english, testament=testament, author=metadata.author, themes=list(metadata.themes), description=metadata.description, chapter_count=metadata.chapter_count, verse_count=metadata.verse_count, ) def chapter_summary_to_proto(summary: ChapterSummaryModel) -> ChapterSummaryProto: """Convert ChapterSummary Pydantic model to proto representation.""" return ChapterSummaryProto( book=summary.book, chapter=summary.chapter, summary=summary.summary, key_events=list(summary.key_events), key_characters=list(summary.key_characters), key_themes=list(summary.key_themes), ) def glossary_term_to_proto(term: GlossaryTermModel) -> GlossaryTermProto: """Convert GlossaryTerm Pydantic model to proto representation.""" return GlossaryTermProto( original=term.original, transliteration=term.transliteration, japanese=term.japanese, english=term.english, notes=term.notes, lemma=term.lemma, inflected_forms=list(term.inflected_forms), strongs_number=term.strongs_number, definition=term.definition, semantic_range=list(term.semantic_range), domain=_theological_domain_to_proto(term.domain), usage_examples=list(term.usage_examples), frequency=term.frequency, related_terms=list(term.related_terms), avoid_translations=list(term.avoid_translations), ) def _equivalence_approach_to_proto(approach: EquivalenceApproachModel) -> EquivalenceApproach: """Convert EquivalenceApproach enum to proto enum.""" approach_map = { "formal": EquivalenceApproach.FORMAL, "dynamic": EquivalenceApproach.DYNAMIC, "optimal": EquivalenceApproach.OPTIMAL, } return approach_map.get(approach.value, EquivalenceApproach.EQUIVALENCE_APPROACH_UNSPECIFIED) def _gender_language_to_proto(gender: GenderLanguageModel) -> GenderLanguage: """Convert GenderLanguage enum to proto enum.""" gender_map = { "traditional": GenderLanguage.TRADITIONAL, "moderate_inclusive": GenderLanguage.MODERATE_INCLUSIVE, "fully_inclusive": GenderLanguage.FULLY_INCLUSIVE, } return gender_map.get(gender.value, GenderLanguage.GENDER_LANGUAGE_UNSPECIFIED) def _readability_level_to_proto(level: ReadabilityLevelModel) -> ReadabilityLevel: """Convert ReadabilityLevel enum to proto enum.""" level_map = { "scholarly": ReadabilityLevel.SCHOLARLY, "general_public": ReadabilityLevel.GENERAL_PUBLIC, "children": ReadabilityLevel.CHILDREN, "web_native": ReadabilityLevel.WEB_NATIVE, } return level_map.get(level.value, ReadabilityLevel.READABILITY_LEVEL_UNSPECIFIED) def _theological_tradition_to_proto(tradition: TheologicalTraditionModel) -> TheologicalTradition: """Convert TheologicalTradition enum to proto enum.""" tradition_map = { "evangelical": TheologicalTradition.EVANGELICAL, "catholic": TheologicalTradition.CATHOLIC, "orthodox": TheologicalTradition.ORTHODOX, "ecumenical": TheologicalTradition.ECUMENICAL, } return tradition_map.get(tradition.value, TheologicalTradition.THEOLOGICAL_TRADITION_UNSPECIFIED) def _term_consistency_to_proto(consistency: TermConsistencyModel) -> TermConsistency: """Convert TermConsistency enum to proto enum.""" consistency_map = { "strict": TermConsistency.STRICT, "balanced": TermConsistency.BALANCED, "contextual": TermConsistency.CONTEXTUAL, } return consistency_map.get(consistency.value, TermConsistency.TERM_CONSISTENCY_UNSPECIFIED) def _formality_level_to_proto(level: FormalityLevelModel) -> FormalityLevel: """Convert FormalityLevel enum to proto enum.""" level_map = { "casual": FormalityLevel.CASUAL, "standard": FormalityLevel.STANDARD, "polite": FormalityLevel.POLITE, "formal": FormalityLevel.FORMAL_LEVEL, } return level_map.get(level.value, FormalityLevel.FORMALITY_LEVEL_UNSPECIFIED) def translation_style_to_proto(style: TranslationStyleModel) -> TranslationStyleProto: """Convert TranslationStyle dataclass to proto representation.""" return TranslationStyleProto( equivalence_approach=_equivalence_approach_to_proto(style.equivalence_approach), gender_language=_gender_language_to_proto(style.gender_language), readability_level=_readability_level_to_proto(style.readability_level), theological_tradition=_theological_tradition_to_proto(style.theological_tradition), textual_basis=style.textual_basis, term_consistency=_term_consistency_to_proto(style.term_consistency), formality_level=_formality_level_to_proto(style.formality_level), avoid_archaic=list(style.avoid_archaic), target_style_reference=style.target_style_reference, additional_instructions=style.additional_instructions, ) def furigana_annotation_to_proto(annotation: FuriganaAnnotation) -> FuriganaAnnotationProto: """Convert FuriganaAnnotation to proto representation.""" return FuriganaAnnotationProto( start_offset=annotation.start_offset, end_offset=annotation.end_offset, base_text=annotation.base_text, reading=annotation.reading, is_standard_reading=annotation.is_standard_reading, reading_source=annotation.reading_source, ) def annotated_word_to_proto( surface_form: str, lemma: str, part_of_speech: str, reading: str, start_offset: int, end_offset: int, alternative_readings: list[str] | None = None, ) -> AnnotatedWordProto: """Create an AnnotatedWordProto from components.""" return AnnotatedWordProto( surface_form=surface_form, lemma=lemma, part_of_speech=part_of_speech, reading=reading, alternative_readings=alternative_readings or [], start_offset=start_offset, end_offset=end_offset, ) def translation_context_to_prompt_input( context: TranslationContextModel, system_prompt: str = "", user_prompt: str = "", model: str = "", prompt_version: str = "v1.0", ) -> TranslationPromptInputProto: """Convert TranslationContext to TranslationPromptInput proto. Args: context: The translation context with verses, metadata, and glossary system_prompt: The system prompt used for translation user_prompt: The user prompt used for translation model: The model name used for translation prompt_version: Version of the prompt template Returns: TranslationPromptInputProto capturing all inputs for reproducibility """ return TranslationPromptInputProto( target_verse=verse_to_proto(context.target_verse), preceding_verses=[verse_to_proto(v) for v in context.preceding_verses], following_verses=[verse_to_proto(v) for v in context.following_verses], chapter_summary=( chapter_summary_to_proto(context.chapter_summary) if context.chapter_summary else None ), book_metadata=( book_metadata_to_proto(context.book_metadata) if context.book_metadata else None ), glossary_terms=[glossary_term_to_proto(t) for t in context.glossary_terms], system_prompt=system_prompt, user_prompt=user_prompt, model=model, prompt_version=prompt_version, request_time=datetime.now(timezone.utc), ) def translation_result_to_proto( output: TranslationOutputModel, text_with_furigana: str = "", furigana_annotations: list[FuriganaAnnotation] | None = None, annotated_words: list[AnnotatedWordProto] | None = None, confidence: ConfidenceLevel = ConfidenceLevel.CONFIDENCE_LEVEL_UNSPECIFIED, confidence_score: float = 0.0, input_hash: str = "", ) -> TranslationResultProto: """Convert TranslationOutput to TranslationResult proto with furigana. Args: output: The translation output from the translator text_with_furigana: Text with inline furigana notation furigana_annotations: Parsed furigana annotations annotated_words: Linguistic word annotations confidence: Confidence level enum confidence_score: Numeric confidence score (0-1) input_hash: Hash of the input for reproducibility tracking Returns: TranslationResultProto with rich annotation data """ return TranslationResultProto( reference=verse_reference_to_proto(output.reference), text=output.text, text_with_inline_furigana=text_with_furigana or output.text, furigana_annotations=[ furigana_annotation_to_proto(a) for a in (furigana_annotations or []) ], annotated_words=annotated_words or [], model=output.model, timestamp=output.timestamp, prompt_version=output.prompt_version, sources_used=list(output.sources_used), glossary_terms_applied=list(output.glossary_terms_applied), confidence=confidence, confidence_score=confidence_score, input_hash=input_hash, ) # ============================================================================= # Converter Functions: Proto -> Pydantic # ============================================================================= def proto_to_verse_reference(proto: VerseReferenceProto) -> VerseReferenceModel: """Convert proto VerseReference to Pydantic model.""" from src.models.verse import VerseReference return VerseReference( book=proto.book, chapter=proto.chapter, verse=proto.verse, ) def proto_to_verse(proto: VerseProto) -> VerseModel: """Convert proto Verse to Pydantic model.""" from src.models.verse import Verse return Verse( reference=proto_to_verse_reference(proto.reference), greek_text=proto.greek_text, greek_translit=proto.greek_translit, hebrew_text=proto.hebrew_text, hebrew_translit=proto.hebrew_translit, japanese_meiji=proto.japanese_meiji, japanese_taisho=proto.japanese_taisho, english_kjv=proto.english_kjv, english_asv=proto.english_asv, ) def proto_to_translation_result(proto: TranslationResultProto) -> TranslationOutputModel: """Convert proto TranslationResult to Pydantic TranslationOutput. Note: This returns the basic TranslationOutput model. The furigana annotations and other rich data are only available in the proto format. """ from src.models.verse import TranslationOutput return TranslationOutput( reference=proto_to_verse_reference(proto.reference), text=proto.text, model=proto.model, timestamp=proto.timestamp, prompt_version=proto.prompt_version, sources_used=list(proto.sources_used), glossary_terms_applied=list(proto.glossary_terms_applied), ) # ============================================================================= # Translation Memory Converters # ============================================================================= def _segment_level_to_proto(level: SegmentLevelModel) -> SegmentLevel: """Convert SegmentLevel enum to proto enum.""" level_map = { "verse": SegmentLevel.VERSE, "phrase": SegmentLevel.PHRASE, } return level_map.get(level.value, SegmentLevel.SEGMENT_LEVEL_UNSPECIFIED) def _match_type_to_proto(match_type: MatchTypeModel) -> MatchType: """Convert MatchType enum to proto enum.""" type_map = { "exact": MatchType.EXACT, "fuzzy": MatchType.FUZZY, "phrase": MatchType.PHRASE, } return type_map.get(match_type.value, MatchType.MATCH_TYPE_UNSPECIFIED) def tm_segment_to_proto(segment: TMSegmentModel) -> TMSegmentProto: """Convert TMSegment Pydantic model to proto representation. Args: segment: The TMSegment to convert. Returns: TMSegmentProto representation. """ return TMSegmentProto( id=segment.id, source_text=segment.source_text, source_normalized=segment.source_normalized, target_text=segment.target_text, reference=verse_reference_to_proto(segment.reference), level=_segment_level_to_proto(segment.level), created_at=segment.created_at, reuse_count=segment.reuse_count, metadata=dict(segment.metadata), ) def tm_match_to_proto(match: TMMatchModel) -> TranslationMemoryMatchProto: """Convert TMMatch Pydantic model to proto representation. Args: match: The TMMatch to convert. Returns: TranslationMemoryMatchProto representation. """ return TranslationMemoryMatchProto( segment=tm_segment_to_proto(match.segment), similarity=match.similarity, match_type=_match_type_to_proto(match.match_type), ) def provenance_to_proto(provenance: TranslationProvenanceModel) -> TranslationProvenanceProto: """Convert TranslationProvenance to proto representation. Args: provenance: The TranslationProvenance to convert. Returns: TranslationProvenanceProto representation. """ return TranslationProvenanceProto( source_texts=dict(provenance.source_texts), source_hashes=dict(provenance.source_hashes), preceding_verses=list(provenance.preceding_verses), following_verses=list(provenance.following_verses), pericope_id=provenance.pericope_id, style_config_hash=provenance.style_config_hash, glossary_hash=provenance.glossary_hash, prompt_version=provenance.prompt_version, model=provenance.model, system_prompt=provenance.system_prompt, user_prompt=provenance.user_prompt, tm_matches_used=list(provenance.tm_matches_used), attempt_number=provenance.attempt_number, started_at=provenance.started_at, completed_at=provenance.completed_at, evaluation_score=provenance.evaluation_score, evaluation_flags=list(provenance.evaluation_flags), chapter_summary=provenance.chapter_summary, book_metadata=dict(provenance.book_metadata), ) # ============================================================================= # Proto -> Model Converters for TM and Provenance # ============================================================================= def _proto_to_segment_level(level: SegmentLevel) -> SegmentLevelModel: """Convert proto SegmentLevel to model enum.""" from src.models.translation_memory import SegmentLevel as SegmentLevelModel level_map = { SegmentLevel.VERSE: SegmentLevelModel.VERSE, SegmentLevel.PHRASE: SegmentLevelModel.PHRASE, } return level_map.get(level, SegmentLevelModel.VERSE) def _proto_to_match_type(match_type: MatchType) -> MatchTypeModel: """Convert proto MatchType to model enum.""" from src.models.translation_memory import MatchType as MatchTypeModel type_map = { MatchType.EXACT: MatchTypeModel.EXACT, MatchType.FUZZY: MatchTypeModel.FUZZY, MatchType.PHRASE: MatchTypeModel.PHRASE, } return type_map.get(match_type, MatchTypeModel.EXACT) def proto_to_tm_segment(proto: TMSegmentProto) -> TMSegmentModel: """Convert proto TMSegment to Pydantic model. Args: proto: The TMSegmentProto to convert. Returns: TMSegment Pydantic model. """ from src.models.translation_memory import TMSegment return TMSegment( id=proto.id, source_text=proto.source_text, source_normalized=proto.source_normalized, target_text=proto.target_text, reference=proto_to_verse_reference(proto.reference), level=_proto_to_segment_level(proto.level), created_at=proto.created_at, reuse_count=proto.reuse_count, metadata=dict(proto.metadata), ) def proto_to_tm_match(proto: TranslationMemoryMatchProto) -> TMMatchModel: """Convert proto TMMatch to Pydantic model. Args: proto: The TranslationMemoryMatchProto to convert. Returns: TMMatch Pydantic model. """ from src.models.translation_memory import TMMatch return TMMatch( segment=proto_to_tm_segment(proto.segment), similarity=proto.similarity, match_type=_proto_to_match_type(proto.match_type), ) def proto_to_provenance(proto: TranslationProvenanceProto) -> TranslationProvenanceModel: """Convert proto TranslationProvenance to model. Args: proto: The TranslationProvenanceProto to convert. Returns: TranslationProvenance model. """ from src.orchestration.provenance import TranslationProvenance return TranslationProvenance( source_texts=dict(proto.source_texts), source_hashes=dict(proto.source_hashes), preceding_verses=list(proto.preceding_verses), following_verses=list(proto.following_verses), pericope_id=proto.pericope_id, style_config_hash=proto.style_config_hash, glossary_hash=proto.glossary_hash, prompt_version=proto.prompt_version, model=proto.model, system_prompt=proto.system_prompt, user_prompt=proto.user_prompt, tm_matches_used=list(proto.tm_matches_used), attempt_number=proto.attempt_number, started_at=proto.started_at, completed_at=proto.completed_at, evaluation_score=proto.evaluation_score, evaluation_flags=list(proto.evaluation_flags), chapter_summary=proto.chapter_summary, book_metadata=dict(proto.book_metadata), )