OPEN SOURCE
software/src/data/storage.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Storage utilities for translations and progress tracking."""23from __future__ import annotations45import json6from datetime import datetime7from pathlib import Path8from typing import TYPE_CHECKING, Any910from src.models.verse import TranslationOutput, VerseReference1112if TYPE_CHECKING:13from src.orchestration.provenance import TranslationProvenance14from src.protos.converters import (15TranslationPromptInputProto,16TranslationResultProto,17)181920class Storage:21"""Manages storage of translations and progress tracking."""2223def __init__(24self,25data_dir: Path | str,26enable_archive: bool = False,27) -> None:28"""Initialize storage.2930Args:31data_dir: Path to the data directory.32enable_archive: Whether to enable proto archive storage.33"""34self.data_dir = Path(data_dir)35self.output_dir = self.data_dir / "output"36self.progress_dir = self.data_dir / "progress"37self.archive_dir = self.data_dir / "archive"38self._enable_archive = enable_archive39self._ensure_dirs()4041@property42def enable_archive(self) -> bool:43"""Get archive enabled status."""44return self._enable_archive4546@enable_archive.setter47def enable_archive(self, value: bool) -> None:48"""Set archive enabled status."""49self._enable_archive = value50if value:51self.archive_dir.mkdir(parents=True, exist_ok=True)5253def _ensure_dirs(self) -> None:54"""Ensure required directories exist."""55self.output_dir.mkdir(parents=True, exist_ok=True)56self.progress_dir.mkdir(parents=True, exist_ok=True)57if self._enable_archive:58self.archive_dir.mkdir(parents=True, exist_ok=True)5960def _load_json(self, path: Path) -> dict[str, Any]:61"""Load a JSON file."""62if not path.exists():63return {}64with path.open("r", encoding="utf-8") as f:65return json.load(f)6667def _save_json(self, path: Path, data: dict[str, Any]) -> None:68"""Save data to a JSON file."""69path.parent.mkdir(parents=True, exist_ok=True)70with path.open("w", encoding="utf-8") as f:71json.dump(data, f, ensure_ascii=False, indent=2, default=str)7273def save_translation(self, output: TranslationOutput) -> None:74"""Save a translation output.7576Args:77output: The translation output to save.78"""79book = output.reference.book80chapter = output.reference.chapter81verse = output.reference.verse8283# Load existing chapter file or create new84chapter_file = self.output_dir / book / f"chapter_{chapter:02d}.json"85chapter_data = self._load_json(chapter_file)8687if "verses" not in chapter_data:88chapter_data["verses"] = {}89chapter_data["book"] = book90chapter_data["chapter"] = chapter9192verse_key = str(verse)93chapter_data["verses"][verse_key] = {94"text": output.text,95"text_with_furigana": output.text_with_furigana,96"furigana_annotations": output.furigana_annotations,97"model": output.model,98"timestamp": output.timestamp.isoformat(),99"prompt_version": output.prompt_version,100"sources_used": output.sources_used,101"glossary_terms_applied": output.glossary_terms_applied,102}103104self._save_json(chapter_file, chapter_data)105self._update_progress(book, chapter, verse)106107def load_translation(self, reference: VerseReference) -> TranslationOutput | None:108"""Load a saved translation.109110Args:111reference: The verse reference to load.112113Returns:114TranslationOutput if found, None otherwise.115"""116chapter_file = (117self.output_dir / reference.book / f"chapter_{reference.chapter:02d}.json"118)119chapter_data = self._load_json(chapter_file)120121verses = chapter_data.get("verses", {})122verse_data = verses.get(str(reference.verse))123if not verse_data:124return None125126return TranslationOutput(127reference=reference,128text=verse_data["text"],129text_with_furigana=verse_data.get("text_with_furigana", ""),130furigana_annotations=verse_data.get("furigana_annotations", []),131model=verse_data["model"],132timestamp=datetime.fromisoformat(verse_data["timestamp"]),133prompt_version=verse_data.get("prompt_version", "v1.0"),134sources_used=verse_data.get("sources_used", []),135glossary_terms_applied=verse_data.get("glossary_terms_applied", []),136)137138def load_chapter_translations(139self, book: str, chapter: int140) -> dict[int, TranslationOutput]:141"""Load all translations for a chapter.142143Returns:144Dictionary mapping verse numbers to TranslationOutput.145"""146chapter_file = self.output_dir / book / f"chapter_{chapter:02d}.json"147chapter_data = self._load_json(chapter_file)148149translations = {}150verses = chapter_data.get("verses", {})151for verse_num_str, verse_data in verses.items():152verse_num = int(verse_num_str)153ref = VerseReference(book=book, chapter=chapter, verse=verse_num)154translations[verse_num] = TranslationOutput(155reference=ref,156text=verse_data["text"],157text_with_furigana=verse_data.get("text_with_furigana", ""),158furigana_annotations=verse_data.get("furigana_annotations", []),159model=verse_data["model"],160timestamp=datetime.fromisoformat(verse_data["timestamp"]),161prompt_version=verse_data.get("prompt_version", "v1.0"),162sources_used=verse_data.get("sources_used", []),163glossary_terms_applied=verse_data.get("glossary_terms_applied", []),164)165166return translations167168def _update_progress(self, book: str, chapter: int, verse: int) -> None:169"""Update progress tracking for a book."""170progress_file = self.progress_dir / f"{book}_progress.json"171progress = self._load_json(progress_file)172173if "completed" not in progress:174progress["completed"] = {}175progress["book"] = book176progress["last_updated"] = None177178chapter_key = str(chapter)179if chapter_key not in progress["completed"]:180progress["completed"][chapter_key] = []181182if verse not in progress["completed"][chapter_key]:183progress["completed"][chapter_key].append(verse)184progress["completed"][chapter_key].sort()185186progress["last_updated"] = datetime.utcnow().isoformat()187self._save_json(progress_file, progress)188189def get_progress(self, book: str) -> dict[str, Any]:190"""Get progress for a book.191192Returns:193Progress dictionary with completed verses per chapter.194"""195progress_file = self.progress_dir / f"{book}_progress.json"196return self._load_json(progress_file)197198def is_verse_translated(self, reference: VerseReference) -> bool:199"""Check if a verse has already been translated."""200progress = self.get_progress(reference.book)201completed = progress.get("completed", {})202chapter_verses = completed.get(str(reference.chapter), [])203return reference.verse in chapter_verses204205def get_completion_stats(self, book: str, total_verses: int) -> dict[str, Any]:206"""Get completion statistics for a book.207208Args:209book: Book name.210total_verses: Total verses expected in the book.211212Returns:213Dictionary with completion statistics.214"""215progress = self.get_progress(book)216completed = progress.get("completed", {})217218completed_count = sum(len(verses) for verses in completed.values())219percentage = (completed_count / total_verses * 100) if total_verses > 0 else 0220221return {222"book": book,223"completed": completed_count,224"total": total_verses,225"percentage": round(percentage, 2),226"chapters_started": len(completed),227}228229def save_translation_with_provenance( # pragma: no cover230self,231output: TranslationOutput,232provenance: TranslationProvenance,233) -> None:234"""Save a translation output with full provenance tracking.235236Args:237output: The translation output to save.238provenance: Full provenance record for this translation.239"""240book = output.reference.book241chapter = output.reference.chapter242verse = output.reference.verse243244# Load existing chapter file or create new245chapter_file = self.output_dir / book / f"chapter_{chapter:02d}.json"246chapter_data = self._load_json(chapter_file)247248if "verses" not in chapter_data:249chapter_data["verses"] = {}250chapter_data["book"] = book251chapter_data["chapter"] = chapter252253verse_key = str(verse)254chapter_data["verses"][verse_key] = {255"text": output.text,256"text_with_furigana": output.text_with_furigana,257"furigana_annotations": output.furigana_annotations,258"model": output.model,259"timestamp": output.timestamp.isoformat(),260"prompt_version": output.prompt_version,261"sources_used": output.sources_used,262"glossary_terms_applied": output.glossary_terms_applied,263"provenance": provenance.to_dict(),264}265266self._save_json(chapter_file, chapter_data)267self._update_progress(book, chapter, verse)268269def load_provenance( # pragma: no cover270self, reference: VerseReference271) -> TranslationProvenance | None:272"""Load provenance for a saved translation.273274Args:275reference: The verse reference to load provenance for.276277Returns:278TranslationProvenance if found, None otherwise.279"""280from src.orchestration.provenance import TranslationProvenance281282chapter_file = (283self.output_dir / reference.book / f"chapter_{reference.chapter:02d}.json"284)285chapter_data = self._load_json(chapter_file)286287verses = chapter_data.get("verses", {})288verse_data = verses.get(str(reference.verse))289if not verse_data:290return None291292provenance_data = verse_data.get("provenance")293if not provenance_data:294return None295296return TranslationProvenance.from_dict(provenance_data)297298# =========================================================================299# Proto Archive Methods300# =========================================================================301302def _get_archive_path(self, reference: VerseReference) -> Path:303"""Get the archive directory path for a verse.304305Args:306reference: The verse reference.307308Returns:309Path to the archive directory for this verse.310"""311return (312self.archive_dir313/ reference.book314/ f"chapter_{reference.chapter:02d}"315)316317def save_translation_archive(318self,319reference: VerseReference,320input_proto: TranslationPromptInputProto,321output_proto: TranslationResultProto,322) -> Path:323"""Save translation input/output protos to archive.324325Args:326reference: The verse reference.327input_proto: The input proto with all translation inputs.328output_proto: The output proto with translation results.329330Returns:331Path to the archive directory where files were saved.332"""333from src.protos.serialization import serialize_proto334335archive_path = self._get_archive_path(reference)336archive_path.mkdir(parents=True, exist_ok=True)337338verse_num = reference.verse339input_file = archive_path / f"verse_{verse_num:02d}_input.msgpack"340output_file = archive_path / f"verse_{verse_num:02d}_output.msgpack"341342input_bytes = serialize_proto(input_proto)343output_bytes = serialize_proto(output_proto)344345with input_file.open("wb") as f:346f.write(input_bytes)347348with output_file.open("wb") as f:349f.write(output_bytes)350351return archive_path352353def load_translation_archive(354self,355reference: VerseReference,356) -> tuple[TranslationPromptInputProto, TranslationResultProto] | None:357"""Load translation input/output protos from archive.358359Args:360reference: The verse reference.361362Returns:363Tuple of (input_proto, output_proto) if found, None otherwise.364"""365from src.protos.converters import (366TranslationPromptInputProto,367TranslationResultProto,368)369from src.protos.serialization import deserialize_proto370371archive_path = self._get_archive_path(reference)372verse_num = reference.verse373input_file = archive_path / f"verse_{verse_num:02d}_input.msgpack"374output_file = archive_path / f"verse_{verse_num:02d}_output.msgpack"375376if not input_file.exists() or not output_file.exists():377return None378379with input_file.open("rb") as f:380input_bytes = f.read()381382with output_file.open("rb") as f:383output_bytes = f.read()384385input_proto = deserialize_proto(input_bytes, TranslationPromptInputProto)386output_proto = deserialize_proto(output_bytes, TranslationResultProto)387388return input_proto, output_proto389