lie-detector / storage /database.py
vaisagan's picture
Deploy complete deception signal detection pipeline
c0efaa1 verified
Raw
History Blame Contribute Delete
6.57 kB
"""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()