OPEN SOURCE
software/src/models/translation_memory.py
Read on this site and download the files you need.
Nichijoyaku was created using AI. A complete review by human biblical-language and biblical-studies specialists has not been established. Read alongside established translations and original-language resources.
← Back to the open-source guide
1"""Translation Memory models for segment storage and matching."""23from __future__ import annotations45import re6import unicodedata7from datetime import datetime8from enum import Enum9from uuid import uuid41011from pydantic import BaseModel, Field1213from src.models.verse import VerseReference141516class SegmentLevel(str, Enum):17"""Level of granularity for a translation segment."""1819VERSE = "verse" # Full verse translation20PHRASE = "phrase" # Sub-verse phrase212223class TMSegment(BaseModel):24"""A translation memory segment storing a source-target pair."""2526id: str = Field(default_factory=lambda: str(uuid4()))27source_text: str = Field(..., description="Original Greek/Hebrew text")28source_normalized: str = Field(29..., description="Normalized form for matching"30)31target_text: str = Field(..., description="Japanese translation")32reference: VerseReference = Field(33..., description="Source verse reference"34)35level: SegmentLevel = Field(36default=SegmentLevel.VERSE, description="Segment granularity"37)38created_at: datetime = Field(default_factory=datetime.utcnow)39reuse_count: int = Field(40default=0, ge=0, description="Number of times this segment was reused"41)42metadata: dict = Field(43default_factory=dict,44description="Additional metadata (model, prompt_version, etc.)",45)4647@classmethod48def create(49cls,50source_text: str,51target_text: str,52reference: VerseReference,53level: SegmentLevel = SegmentLevel.VERSE,54metadata: dict | None = None,55) -> TMSegment:56"""Create a new segment with automatic normalization.5758Args:59source_text: Original source text.60target_text: Translated target text.61reference: Verse reference.62level: Segment granularity level.63metadata: Additional metadata.6465Returns:66New TMSegment instance.67"""68return cls(69source_text=source_text,70source_normalized=normalize_text(source_text),71target_text=target_text,72reference=reference,73level=level,74metadata=metadata or {},75)7677def increment_reuse(self) -> None:78"""Increment the reuse counter."""79self.reuse_count += 1808182class MatchType(str, Enum):83"""Type of translation memory match."""8485EXACT = "exact" # 100% match86FUZZY = "fuzzy" # Partial match (configurable threshold)87PHRASE = "phrase" # Phrase-level match within verse888990class TMMatch(BaseModel):91"""A match result from translation memory lookup."""9293segment: TMSegment = Field(..., description="The matched segment")94similarity: float = Field(95..., ge=0.0, le=1.0, description="Match similarity score"96)97match_type: MatchType = Field(..., description="Type of match")9899@property100def is_exact(self) -> bool:101"""Check if this is an exact match."""102return self.match_type == MatchType.EXACT103104@property105def is_usable(self) -> bool:106"""Check if this match is usable (above minimum threshold)."""107return self.similarity >= 0.7108109110def normalize_text(text: str) -> str:111"""Normalize text for matching.112113Normalizes Unicode, removes punctuation, lowercases,114and collapses whitespace.115116Args:117text: Text to normalize.118119Returns:120Normalized text string.121"""122# Unicode normalization (NFC)123text = unicodedata.normalize("NFC", text)124125# Lowercase126text = text.lower()127128# Remove punctuation (keep letters, numbers, spaces)129text = re.sub(r"[^\w\s]", "", text)130131# Collapse whitespace132text = " ".join(text.split())133134return text135136137def calculate_similarity(text1: str, text2: str) -> float:138"""Calculate similarity between two texts using Levenshtein ratio.139140Args:141text1: First text (normalized).142text2: Second text (normalized).143144Returns:145Similarity score between 0.0 and 1.0.146"""147if text1 == text2:148return 1.0149150if not text1 or not text2:151return 0.0152153# Use character-level Levenshtein distance ratio154len1, len2 = len(text1), len(text2)155max_len = max(len1, len2)156157# Unreachable: two empty strings are equal and one empty string returned158# 0.0 above. Kept as a guard against a zero division if that changes.159if max_len == 0: # pragma: no cover160return 1.0161162# Simple Levenshtein implementation163distance = _levenshtein_distance(text1, text2)164return 1.0 - (distance / max_len)165166167def _levenshtein_distance(s1: str, s2: str) -> int:168"""Calculate Levenshtein distance between two strings.169170Args:171s1: First string.172s2: Second string.173174Returns:175Edit distance between the strings.176"""177if len(s1) < len(s2):178return _levenshtein_distance(s2, s1)179180if len(s2) == 0:181return len(s1)182183previous_row = range(len(s2) + 1)184for i, c1 in enumerate(s1):185current_row = [i + 1]186for j, c2 in enumerate(s2):187insertions = previous_row[j + 1] + 1188deletions = current_row[j] + 1189substitutions = previous_row[j] + (c1 != c2)190current_row.append(min(insertions, deletions, substitutions))191previous_row = current_row192193return previous_row[-1]194195196class TMStatistics(BaseModel):197"""Statistics about the translation memory."""198199total_segments: int = 0200verse_segments: int = 0201phrase_segments: int = 0202total_reuses: int = 0203average_reuse: float = 0.0204books_covered: list[str] = Field(default_factory=list)205most_reused_segments: list[str] = Field(default_factory=list)206207@classmethod208def from_segments(cls, segments: list[TMSegment]) -> TMStatistics:209"""Calculate statistics from a list of segments.210211Args:212segments: List of TM segments.213214Returns:215TMStatistics instance.216"""217if not segments:218return cls()219220verse_count = sum(1 for s in segments if s.level == SegmentLevel.VERSE)221phrase_count = sum(1 for s in segments if s.level == SegmentLevel.PHRASE)222total_reuses = sum(s.reuse_count for s in segments)223avg_reuse = total_reuses / len(segments) if segments else 0.0224225books = sorted(set(s.reference.book for s in segments))226227# Get most reused segments (top 5)228sorted_by_reuse = sorted(segments, key=lambda s: s.reuse_count, reverse=True)229most_reused = [s.id for s in sorted_by_reuse[:5] if s.reuse_count > 0]230231return cls(232total_segments=len(segments),233verse_segments=verse_count,234phrase_segments=phrase_count,235total_reuses=total_reuses,236average_reuse=avg_reuse,237books_covered=books,238most_reused_segments=most_reused,239)240