import json import sqlite3 from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, List, Optional from .models import SentenceBoundary, SentenceBoundaryDocument from .store import normalize_video_hash def utc_now() -> str: return datetime.now(timezone.utc).isoformat() class VideoRepository: def __init__(self, database_path: Path): self.database_path = Path(database_path) self.database_path.parent.mkdir(parents=True, exist_ok=True) self._initialize() def _connect(self) -> sqlite3.Connection: connection = sqlite3.connect(self.database_path, timeout=30) connection.row_factory = sqlite3.Row connection.execute("PRAGMA foreign_keys = ON") connection.execute("PRAGMA journal_mode = WAL") return connection def _initialize(self) -> None: with self._connect() as connection: connection.executescript( """ CREATE TABLE IF NOT EXISTS videos ( video_hash TEXT PRIMARY KEY, title TEXT NOT NULL, filename TEXT NOT NULL, stored_filename TEXT NOT NULL, content_type TEXT NOT NULL, size_bytes INTEGER NOT NULL, duration_ms INTEGER, language TEXT, status TEXT NOT NULL, error_message TEXT, algorithm_version TEXT, transcription TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS sentences ( video_hash TEXT NOT NULL, sentence_index INTEGER NOT NULL, start_ms INTEGER NOT NULL, end_ms INTEGER NOT NULL, text TEXT, language TEXT, reference_speech_duration_ms INTEGER, PRIMARY KEY (video_hash, sentence_index), FOREIGN KEY (video_hash) REFERENCES videos(video_hash) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS attempts ( attempt_id TEXT PRIMARY KEY, video_hash TEXT NOT NULL, sentence_index INTEGER NOT NULL, result_json TEXT NOT NULL, audio_filename TEXT, created_at TEXT NOT NULL, FOREIGN KEY (video_hash) REFERENCES videos(video_hash) ON DELETE CASCADE ); """ ) def upsert_upload( self, *, video_hash: str, title: str, filename: str, stored_filename: str, content_type: str, size_bytes: int, language: Optional[str], ) -> None: normalized_hash = normalize_video_hash(video_hash) now = utc_now() with self._connect() as connection: existing = connection.execute( "SELECT created_at FROM videos WHERE video_hash = ?", (normalized_hash,), ).fetchone() created_at = existing["created_at"] if existing else now connection.execute( """ INSERT INTO videos ( video_hash, title, filename, stored_filename, content_type, size_bytes, duration_ms, language, status, error_message, algorithm_version, transcription, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, NULL, ?, 'uploaded', NULL, NULL, NULL, ?, ?) ON CONFLICT(video_hash) DO UPDATE SET title = excluded.title, filename = excluded.filename, stored_filename = excluded.stored_filename, content_type = excluded.content_type, size_bytes = excluded.size_bytes, language = excluded.language, status = 'uploaded', error_message = NULL, updated_at = excluded.updated_at """, ( normalized_hash, title, filename, stored_filename, content_type, size_bytes, language, created_at, now, ), ) def mark_processing(self, video_hash: str) -> None: self._update_status(video_hash, "processing", None) def mark_failed(self, video_hash: str, message: str) -> None: self._update_status(video_hash, "failed", message[:4000]) def _update_status(self, video_hash: str, status: str, error_message: Optional[str]) -> None: with self._connect() as connection: connection.execute( "UPDATE videos SET status = ?, error_message = ?, updated_at = ? WHERE video_hash = ?", (status, error_message, utc_now(), normalize_video_hash(video_hash)), ) def save_processing_result( self, document: SentenceBoundaryDocument, transcription: Optional[str], ) -> None: with self._connect() as connection: connection.execute( "DELETE FROM sentences WHERE video_hash = ?", (document.video_hash,), ) connection.executemany( """ INSERT INTO sentences ( video_hash, sentence_index, start_ms, end_ms, text, language, reference_speech_duration_ms ) VALUES (?, ?, ?, ?, ?, ?, ?) """, [ ( document.video_hash, sentence.index, sentence.start_ms, sentence.end_ms, sentence.text, sentence.language, sentence.reference_speech_duration_ms, ) for sentence in document.sentences ], ) connection.execute( """ UPDATE videos SET duration_ms = ?, status = 'ready', error_message = NULL, algorithm_version = ?, transcription = ?, updated_at = ? WHERE video_hash = ? """, ( document.duration_ms, document.algorithm_version, transcription, utc_now(), document.video_hash, ), ) def list_videos(self) -> List[Dict[str, Any]]: with self._connect() as connection: rows = connection.execute( """ SELECT v.*, COUNT(s.sentence_index) AS sentence_count FROM videos v LEFT JOIN sentences s ON s.video_hash = v.video_hash GROUP BY v.video_hash ORDER BY v.created_at DESC """ ).fetchall() return [dict(row) for row in rows] def get_video(self, video_hash: str) -> Optional[Dict[str, Any]]: with self._connect() as connection: row = connection.execute( """ SELECT v.*, COUNT(s.sentence_index) AS sentence_count FROM videos v LEFT JOIN sentences s ON s.video_hash = v.video_hash WHERE v.video_hash = ? GROUP BY v.video_hash """, (normalize_video_hash(video_hash),), ).fetchone() return dict(row) if row else None def get_document(self, video_hash: str) -> Optional[SentenceBoundaryDocument]: video = self.get_video(video_hash) if video is None or video["duration_ms"] is None: return None with self._connect() as connection: rows = connection.execute( "SELECT * FROM sentences WHERE video_hash = ? ORDER BY sentence_index", (normalize_video_hash(video_hash),), ).fetchall() return SentenceBoundaryDocument( video_hash=video["video_hash"], duration_ms=video["duration_ms"], algorithm_version=video["algorithm_version"] or "unknown", sentences=[ SentenceBoundary( index=row["sentence_index"], start_ms=row["start_ms"], end_ms=row["end_ms"], text=row["text"], language=row["language"], reference_speech_duration_ms=row["reference_speech_duration_ms"], ) for row in rows ], ) def update_sentence_text( self, video_hash: str, sentence_index: int, text: str, language: Optional[str], ) -> Optional[SentenceBoundary]: normalized_hash = normalize_video_hash(video_hash) with self._connect() as connection: cursor = connection.execute( """ UPDATE sentences SET text = ?, language = COALESCE(?, language) WHERE video_hash = ? AND sentence_index = ? """, (text.strip(), language, normalized_hash, sentence_index), ) if cursor.rowcount == 0: return None connection.execute( "UPDATE videos SET updated_at = ? WHERE video_hash = ?", (utc_now(), normalized_hash), ) row = connection.execute( "SELECT * FROM sentences WHERE video_hash = ? AND sentence_index = ?", (normalized_hash, sentence_index), ).fetchone() return SentenceBoundary( index=row["sentence_index"], start_ms=row["start_ms"], end_ms=row["end_ms"], text=row["text"], language=row["language"], reference_speech_duration_ms=row["reference_speech_duration_ms"], ) def record_attempt( self, *, attempt_id: str, video_hash: str, sentence_index: int, result: Dict[str, Any], audio_filename: Optional[str], ) -> None: with self._connect() as connection: connection.execute( """ INSERT INTO attempts ( attempt_id, video_hash, sentence_index, result_json, audio_filename, created_at ) VALUES (?, ?, ?, ?, ?, ?) """, ( attempt_id, normalize_video_hash(video_hash), sentence_index, json.dumps(result, ensure_ascii=False), audio_filename, utc_now(), ), ) def delete_video(self, video_hash: str) -> Optional[Dict[str, Any]]: video = self.get_video(video_hash) if video is None: return None with self._connect() as connection: connection.execute( "DELETE FROM videos WHERE video_hash = ?", (normalize_video_hash(video_hash),), ) return video