OPEN SOURCE
software/src/validation/checks.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Validation checks for translations."""23from __future__ import annotations45from dataclasses import dataclass, field6from typing import TYPE_CHECKING78if TYPE_CHECKING:9from src.context.glossary import GlossaryManager10from src.models.verse import TranslationOutput111213@dataclass14class ValidationResult:15"""Result of a validation check."""1617passed: bool18check_name: str19message: str20details: dict = field(default_factory=dict)212223@dataclass24class ValidationReport:25"""Full validation report for a translation."""2627results: list[ValidationResult] = field(default_factory=list)2829@property30def all_passed(self) -> bool:31"""Check if all validations passed."""32return all(r.passed for r in self.results)3334@property35def failed_checks(self) -> list[ValidationResult]:36"""Get list of failed checks."""37return [r for r in self.results if not r.passed]3839def add_result(self, result: ValidationResult) -> None:40"""Add a validation result."""41self.results.append(result)424344class TranslationValidator:45"""Validates translation outputs."""4647def __init__(48self,49glossary: GlossaryManager | None = None,50min_length: int = 5,51max_length: int = 2000,52) -> None:53"""Initialize the validator.5455Args:56glossary: Glossary manager for term consistency checks.57min_length: Minimum translation length.58max_length: Maximum translation length.59"""60self.glossary = glossary61self.min_length = min_length62self.max_length = max_length6364def validate(self, output: TranslationOutput) -> ValidationReport:65"""Run all validation checks on a translation.6667Args:68output: Translation output to validate.6970Returns:71ValidationReport with all check results.72"""73report = ValidationReport()7475report.add_result(self._check_not_empty(output))76report.add_result(self._check_length(output))77report.add_result(self._check_no_english_only(output))78report.add_result(self._check_has_japanese(output))7980if self.glossary:81report.add_result(self._check_glossary_consistency(output))8283return report8485def _check_not_empty(self, output: TranslationOutput) -> ValidationResult:86"""Check that translation is not empty."""87if not output.text or not output.text.strip():88return ValidationResult(89passed=False,90check_name="not_empty",91message="Translation is empty",92)93return ValidationResult(94passed=True,95check_name="not_empty",96message="Translation is not empty",97)9899def _check_length(self, output: TranslationOutput) -> ValidationResult:100"""Check translation length is within bounds."""101length = len(output.text)102103if length < self.min_length:104return ValidationResult(105passed=False,106check_name="length",107message=f"Translation too short ({length} < {self.min_length})",108details={"length": length, "min": self.min_length},109)110111if length > self.max_length:112return ValidationResult(113passed=False,114check_name="length",115message=f"Translation too long ({length} > {self.max_length})",116details={"length": length, "max": self.max_length},117)118119return ValidationResult(120passed=True,121check_name="length",122message=f"Translation length OK ({length})",123details={"length": length},124)125126def _check_no_english_only(self, output: TranslationOutput) -> ValidationResult:127"""Check that translation is not English-only."""128text = output.text.strip()129130# Check if text contains any Japanese characters131has_japanese = any(132"\u3040" <= char <= "\u309f" # Hiragana133or "\u30a0" <= char <= "\u30ff" # Katakana134or "\u4e00" <= char <= "\u9fff" # CJK135for char in text136)137138if not has_japanese:139return ValidationResult(140passed=False,141check_name="no_english_only",142message="Translation appears to be English only",143)144145return ValidationResult(146passed=True,147check_name="no_english_only",148message="Translation contains Japanese characters",149)150151def _check_has_japanese(self, output: TranslationOutput) -> ValidationResult:152"""Check that translation has sufficient Japanese content."""153text = output.text.strip()154155# Count Japanese characters156japanese_chars = sum(1571158for char in text159if (160"\u3040" <= char <= "\u309f" # Hiragana161or "\u30a0" <= char <= "\u30ff" # Katakana162or "\u4e00" <= char <= "\u9fff" # CJK163)164)165166total_chars = len(text)167ratio = japanese_chars / total_chars if total_chars > 0 else 0168169if ratio < 0.3:170return ValidationResult(171passed=False,172check_name="has_japanese",173message=f"Japanese character ratio too low ({ratio:.1%})",174details={"ratio": ratio, "japanese_chars": japanese_chars},175)176177return ValidationResult(178passed=True,179check_name="has_japanese",180message=f"Japanese character ratio OK ({ratio:.1%})",181details={"ratio": ratio, "japanese_chars": japanese_chars},182)183184def _check_glossary_consistency(185self, output: TranslationOutput186) -> ValidationResult:187"""Check that glossary terms are used consistently.188189Note: This method should only be called when self.glossary is set.190The validate() method handles this check.191"""192assert self.glossary is not None193# Get terms that were supposed to be applied194applied_terms = output.glossary_terms_applied195196if not applied_terms:197return ValidationResult(198passed=True,199check_name="glossary_consistency",200message="No glossary terms expected",201)202203# Check if Japanese translations are present204missing = []205for term_original in applied_terms:206term = self.glossary.get_by_original(term_original)207if term and term.japanese not in output.text:208missing.append(term_original)209210if missing:211return ValidationResult(212passed=False,213check_name="glossary_consistency",214message=f"Missing expected glossary terms: {', '.join(missing)}",215details={"missing": missing},216)217218return ValidationResult(219passed=True,220check_name="glossary_consistency",221message="All expected glossary terms present",222)223