Spaces:
Runtime error
Runtime error
| """DuckDB persistence layer for all analysis runs.""" | |
| import json | |
| import time | |
| from pathlib import Path | |
| from typing import Any, Optional | |
| import duckdb | |
| import pandas as pd | |
| class Database: | |
| def __init__(self, path: str = "lie_detector.duckdb"): | |
| self.path = path | |
| self.con = duckdb.connect(path) | |
| self._init_schema() | |
| # ------------------------------------------------------------------ | |
| # Schema | |
| # ------------------------------------------------------------------ | |
| def _init_schema(self) -> None: | |
| self.con.execute(""" | |
| CREATE TABLE IF NOT EXISTS runs ( | |
| run_id VARCHAR PRIMARY KEY, | |
| created_at DOUBLE NOT NULL, | |
| source_path VARCHAR, | |
| duration_sec DOUBLE, | |
| unified_score DOUBLE, | |
| facial_score DOUBLE, | |
| audio_score DOUBLE, | |
| linguistic_score DOUBLE, | |
| facial_detail JSON, | |
| audio_detail JSON, | |
| linguistic_detail JSON, | |
| fusion_weights JSON, | |
| metadata JSON | |
| ) | |
| """) | |
| self.con.execute(""" | |
| CREATE TABLE IF NOT EXISTS frame_signals ( | |
| run_id VARCHAR, | |
| frame_idx INTEGER, | |
| timestamp_s DOUBLE, | |
| blink BOOLEAN, | |
| asymmetric_smile DOUBLE, | |
| micro_expression BOOLEAN, | |
| eye_gaze_shift DOUBLE, | |
| lip_compress DOUBLE, | |
| head_nod DOUBLE | |
| ) | |
| """) | |
| self.con.execute(""" | |
| CREATE TABLE IF NOT EXISTS sentence_signals ( | |
| run_id VARCHAR, | |
| sentence_idx INTEGER, | |
| start_s DOUBLE, | |
| end_s DOUBLE, | |
| text VARCHAR, | |
| pronoun_distance DOUBLE, | |
| neg_emotion DOUBLE, | |
| cognitive_complexity DOUBLE, | |
| hedging DOUBLE, | |
| coherence DOUBLE, | |
| over_explanation DOUBLE, | |
| contradiction DOUBLE, | |
| llm_score DOUBLE, | |
| llm_reasoning VARCHAR | |
| ) | |
| """) | |
| self.con.execute(""" | |
| CREATE TABLE IF NOT EXISTS audio_segments ( | |
| run_id VARCHAR, | |
| segment_idx INTEGER, | |
| start_s DOUBLE, | |
| end_s DOUBLE, | |
| pitch_mean DOUBLE, | |
| pitch_std DOUBLE, | |
| speech_rate DOUBLE, | |
| pause_count INTEGER, | |
| energy_mean DOUBLE, | |
| zcr_mean DOUBLE, | |
| filler_count INTEGER | |
| ) | |
| """) | |
| # ------------------------------------------------------------------ | |
| # Write | |
| # ------------------------------------------------------------------ | |
| def save_run( | |
| self, | |
| run_id: str, | |
| source_path: str, | |
| duration_sec: float, | |
| unified_score: float, | |
| facial_score: float, | |
| audio_score: float, | |
| linguistic_score: float, | |
| facial_detail: dict, | |
| audio_detail: dict, | |
| linguistic_detail: dict, | |
| fusion_weights: dict, | |
| metadata: Optional[dict] = None, | |
| ) -> None: | |
| self.con.execute( | |
| """ | |
| INSERT OR REPLACE INTO runs VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) | |
| """, | |
| [ | |
| run_id, | |
| time.time(), | |
| source_path, | |
| duration_sec, | |
| unified_score, | |
| facial_score, | |
| audio_score, | |
| linguistic_score, | |
| json.dumps(facial_detail), | |
| json.dumps(audio_detail), | |
| json.dumps(linguistic_detail), | |
| json.dumps(fusion_weights), | |
| json.dumps(metadata or {}), | |
| ], | |
| ) | |
| def save_frame_signals(self, run_id: str, frames: list[dict]) -> None: | |
| if not frames: | |
| return | |
| df = pd.DataFrame(frames) | |
| df.insert(0, "run_id", run_id) | |
| self.con.execute("INSERT INTO frame_signals SELECT * FROM df") | |
| def save_sentence_signals(self, run_id: str, sentences: list[dict]) -> None: | |
| if not sentences: | |
| return | |
| df = pd.DataFrame(sentences) | |
| df.insert(0, "run_id", run_id) | |
| self.con.execute("INSERT INTO sentence_signals SELECT * FROM df") | |
| def save_audio_segments(self, run_id: str, segments: list[dict]) -> None: | |
| if not segments: | |
| return | |
| df = pd.DataFrame(segments) | |
| df.insert(0, "run_id", run_id) | |
| self.con.execute("INSERT INTO audio_segments SELECT * FROM df") | |
| # ------------------------------------------------------------------ | |
| # Read | |
| # ------------------------------------------------------------------ | |
| def list_runs(self) -> pd.DataFrame: | |
| return self.con.execute( | |
| """ | |
| SELECT run_id, created_at, source_path, duration_sec, | |
| unified_score, facial_score, audio_score, linguistic_score | |
| FROM runs ORDER BY created_at DESC | |
| """ | |
| ).df() | |
| def get_run(self, run_id: str) -> Optional[dict]: | |
| rows = self.con.execute( | |
| "SELECT * FROM runs WHERE run_id = ?", [run_id] | |
| ).fetchall() | |
| if not rows: | |
| return None | |
| cols = [d[0] for d in self.con.description] | |
| row = dict(zip(cols, rows[0])) | |
| for field in ("facial_detail", "audio_detail", "linguistic_detail", | |
| "fusion_weights", "metadata"): | |
| if row.get(field): | |
| row[field] = json.loads(row[field]) | |
| return row | |
| def get_frame_signals(self, run_id: str) -> pd.DataFrame: | |
| return self.con.execute( | |
| "SELECT * FROM frame_signals WHERE run_id = ? ORDER BY frame_idx", | |
| [run_id], | |
| ).df() | |
| def get_sentence_signals(self, run_id: str) -> pd.DataFrame: | |
| return self.con.execute( | |
| "SELECT * FROM sentence_signals WHERE run_id = ? ORDER BY sentence_idx", | |
| [run_id], | |
| ).df() | |
| def get_audio_segments(self, run_id: str) -> pd.DataFrame: | |
| return self.con.execute( | |
| "SELECT * FROM audio_segments WHERE run_id = ? ORDER BY segment_idx", | |
| [run_id], | |
| ).df() | |
| def close(self) -> None: | |
| self.con.close() | |