feat: add migrations, test scripts, and utility tools

- Add database migrations (006-028) for face recognition, identity, file_uuid
- Add test scripts for ASR, face, search, processing
- Add portal frontend (Tauri)
- Add config, benchmark, and monitoring utilities
- Add model checkpoints and pretrained model references
This commit is contained in:
Warren
2026-04-30 15:11:53 +08:00
parent 4d75b2e251
commit b54c2def30
192 changed files with 46721 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
-- ================================================================
-- Migration 006: Face Recognition Tables
-- Version: 006
-- Date: 2026-03-30
-- Description: Add tables for face recognition feature storage
-- Includes face embeddings, identities, and clusters
-- ================================================================
-- 6.1: Enable pgvector extension if not already enabled
CREATE EXTENSION IF NOT EXISTS vector;
-- 6.2: Create face_identities table
CREATE TABLE IF NOT EXISTS face_identities (
id SERIAL PRIMARY KEY,
face_id VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255),
embedding VECTOR(512), -- InsightFace default embedding dimension
attributes JSONB,
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
-- Indexes for performance
CONSTRAINT face_identities_face_id_key UNIQUE (face_id)
);
-- 6.3: Create face_detections table
CREATE TABLE IF NOT EXISTS face_detections (
id SERIAL PRIMARY KEY,
video_uuid VARCHAR(255) NOT NULL,
frame_number BIGINT NOT NULL,
timestamp_secs DOUBLE PRECISION NOT NULL,
face_id VARCHAR(255),
x INTEGER NOT NULL,
y INTEGER NOT NULL,
width INTEGER NOT NULL,
height INTEGER NOT NULL,
confidence DOUBLE PRECISION NOT NULL,
embedding VECTOR(512),
attributes JSONB,
identity_id INTEGER REFERENCES face_identities(id) ON DELETE SET NULL,
identity_confidence DOUBLE PRECISION,
cluster_id VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Ensure unique detection per frame
CONSTRAINT unique_detection_per_frame UNIQUE (video_uuid, frame_number, x, y, width, height)
);
-- 6.4: Create face_clusters table
CREATE TABLE IF NOT EXISTS face_clusters (
id SERIAL PRIMARY KEY,
cluster_id VARCHAR(255) NOT NULL,
video_uuid VARCHAR(255) NOT NULL,
centroid VECTOR(512),
size INTEGER NOT NULL DEFAULT 0,
representative_face_id VARCHAR(255),
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Constraints
CONSTRAINT face_clusters_cluster_id_key UNIQUE (cluster_id)
);
-- 6.5: Create face_recognition_results table
CREATE TABLE IF NOT EXISTS face_recognition_results (
id SERIAL PRIMARY KEY,
video_uuid VARCHAR(255) NOT NULL UNIQUE,
frame_count BIGINT NOT NULL DEFAULT 0,
fps DOUBLE PRECISION NOT NULL DEFAULT 0.0,
total_faces INTEGER NOT NULL DEFAULT 0,
recognized_faces INTEGER NOT NULL DEFAULT 0,
clusters_count INTEGER NOT NULL DEFAULT 0,
result_data JSONB NOT NULL,
processing_time_secs DOUBLE PRECISION,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Constraints
CONSTRAINT face_recognition_results_video_uuid_key UNIQUE (video_uuid)
);
-- 6.6: Create face_similarity_search function
CREATE OR REPLACE FUNCTION find_similar_faces(
query_embedding VECTOR(512),
similarity_threshold DOUBLE PRECISION DEFAULT 0.6,
limit_count INTEGER DEFAULT 10
)
RETURNS TABLE (
face_id VARCHAR(255),
name VARCHAR(255),
similarity DOUBLE PRECISION,
attributes JSONB,
metadata JSONB
) AS $$
BEGIN
RETURN QUERY
SELECT
fi.face_id,
fi.name,
1 - (fi.embedding <=> query_embedding) AS similarity,
fi.attributes,
fi.metadata
FROM face_identities fi
WHERE fi.is_active = TRUE
AND fi.embedding IS NOT NULL
AND 1 - (fi.embedding <=> query_embedding) >= similarity_threshold
ORDER BY fi.embedding <=> query_embedding
LIMIT limit_count;
END;
$$ LANGUAGE plpgsql;
-- 6.7: Create function to update face cluster centroids
CREATE OR REPLACE FUNCTION update_cluster_centroid(cluster_uuid VARCHAR(255))
RETURNS VOID AS $$
DECLARE
new_centroid VECTOR(512);
BEGIN
-- Calculate new centroid from all face embeddings in the cluster
SELECT AVG(embedding) INTO new_centroid
FROM face_detections
WHERE cluster_id = cluster_uuid
AND embedding IS NOT NULL;
-- Update cluster centroid
UPDATE face_clusters
SET centroid = new_centroid,
size = (SELECT COUNT(*) FROM face_detections WHERE cluster_id = cluster_uuid)
WHERE cluster_id = cluster_uuid;
END;
$$ LANGUAGE plpgsql;
-- 6.8: Create function to find or create face identity
CREATE OR REPLACE FUNCTION find_or_create_face_identity(
p_face_id VARCHAR(255),
p_name VARCHAR(255) DEFAULT NULL,
p_embedding VECTOR(512) DEFAULT NULL,
p_attributes JSONB DEFAULT NULL,
p_metadata JSONB DEFAULT '{}'::jsonb
)
RETURNS INTEGER AS $$
DECLARE
v_id INTEGER;
BEGIN
-- Try to find existing face identity
SELECT id INTO v_id
FROM face_identities
WHERE face_id = p_face_id;
IF v_id IS NULL THEN
-- Create new face identity
INSERT INTO face_identities (face_id, name, embedding, attributes, metadata)
VALUES (p_face_id, p_name, p_embedding, p_attributes, p_metadata)
RETURNING id INTO v_id;
ELSE
-- Update existing face identity
UPDATE face_identities
SET
name = COALESCE(p_name, name),
embedding = COALESCE(p_embedding, embedding),
attributes = COALESCE(p_attributes, attributes),
metadata = COALESCE(p_metadata, metadata),
updated_at = CURRENT_TIMESTAMP
WHERE id = v_id;
END IF;
RETURN v_id;
END;
$$ LANGUAGE plpgsql;
-- 6.9: Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_face_detections_video_uuid ON face_detections(video_uuid);
CREATE INDEX IF NOT EXISTS idx_face_detections_face_id ON face_detections(face_id);
CREATE INDEX IF NOT EXISTS idx_face_detections_frame ON face_detections(video_uuid, frame_number);
CREATE INDEX IF NOT EXISTS idx_face_detections_identity ON face_detections(identity_id);
CREATE INDEX IF NOT EXISTS idx_face_detections_cluster ON face_detections(cluster_id);
CREATE INDEX IF NOT EXISTS idx_face_clusters_video_uuid ON face_clusters(video_uuid);
CREATE INDEX IF NOT EXISTS idx_face_recognition_results_created_at ON face_recognition_results(created_at);
-- 6.10: Create indexes for vector similarity search
CREATE INDEX IF NOT EXISTS idx_face_identities_embedding
ON face_identities USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
CREATE INDEX IF NOT EXISTS idx_face_detections_embedding
ON face_detections USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 6.11: Add comments
COMMENT ON TABLE face_identities IS 'Stores registered face identities with embeddings';
COMMENT ON TABLE face_detections IS 'Stores individual face detections from videos';
COMMENT ON TABLE face_clusters IS 'Stores face clusters from video analysis';
COMMENT ON TABLE face_recognition_results IS 'Stores face recognition processing results';
COMMENT ON FUNCTION find_similar_faces IS 'Finds similar faces based on embedding similarity';
COMMENT ON FUNCTION update_cluster_centroid IS 'Updates cluster centroid from member embeddings';
COMMENT ON FUNCTION find_or_create_face_identity IS 'Finds or creates a face identity record';
-- 6.12: Create trigger to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create triggers only if they don't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'update_face_identities_updated_at'
AND tgrelid = 'face_identities'::regclass
) THEN
CREATE TRIGGER update_face_identities_updated_at
BEFORE UPDATE ON face_identities
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'update_face_recognition_results_updated_at'
AND tgrelid = 'face_recognition_results'::regclass
) THEN
CREATE TRIGGER update_face_recognition_results_updated_at
BEFORE UPDATE ON face_recognition_results
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
END IF;
END $$;

View File

@@ -0,0 +1,328 @@
-- ================================================================
-- Migration 007: Person Identity Integration Tables
-- Version: 007
-- Date: 2026-04-09
-- Description: Add tables for person identity integration
-- Links face recognition and speaker diarization
-- Enables person tracking across video chunks
-- ================================================================
-- 7.1: Create person_identities table
CREATE TABLE IF NOT EXISTS person_identities (
id SERIAL PRIMARY KEY,
person_id VARCHAR(255) NOT NULL UNIQUE,
-- Identity associations
face_identity_id INTEGER REFERENCES face_identities(id) ON DELETE SET NULL,
speaker_id VARCHAR(64), -- SPEAKER_00, SPEAKER_01, etc.
-- Association info
video_uuid VARCHAR(255) NOT NULL,
confidence DOUBLE PRECISION DEFAULT 0.0 CHECK (confidence >= 0.0 AND confidence <= 1.0),
-- Metadata
name VARCHAR(255), -- Person name (manually annotated)
metadata JSONB DEFAULT '{}'::jsonb,
-- Time tracking
first_appearance_time DOUBLE PRECISION,
last_appearance_time DOUBLE PRECISION,
total_appearance_duration DOUBLE PRECISION DEFAULT 0.0,
appearance_count INTEGER DEFAULT 0,
-- Audit fields
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
is_confirmed BOOLEAN DEFAULT FALSE, -- User-confirmed identity
-- Constraints
CONSTRAINT unique_person_identity UNIQUE (video_uuid, face_identity_id, speaker_id),
CONSTRAINT valid_time_range CHECK (
first_appearance_time IS NULL OR
last_appearance_time IS NULL OR
last_appearance_time >= first_appearance_time
)
);
-- 7.2: Create person_appearances table
CREATE TABLE IF NOT EXISTS person_appearances (
id SERIAL PRIMARY KEY,
person_id VARCHAR(255) NOT NULL REFERENCES person_identities(person_id) ON DELETE CASCADE,
-- Appearance info
video_uuid VARCHAR(255) NOT NULL,
start_time DOUBLE PRECISION NOT NULL CHECK (start_time >= 0),
end_time DOUBLE PRECISION NOT NULL CHECK (end_time >= 0),
duration DOUBLE PRECISION NOT NULL CHECK (duration > 0),
-- Source references
face_detection_id INTEGER REFERENCES face_detections(id) ON DELETE SET NULL,
asrx_segment_start DOUBLE PRECISION, -- Reference to ASRX segment
asrx_segment_end DOUBLE PRECISION,
-- Metadata
confidence DOUBLE PRECISION DEFAULT 0.0 CHECK (confidence >= 0.0 AND confidence <= 1.0),
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Constraints
CONSTRAINT valid_appearance_time CHECK (end_time > start_time),
CONSTRAINT valid_duration CHECK (end_time - start_time = duration)
);
-- 7.3: Create indexes for performance
-- Person identities indexes
CREATE INDEX IF NOT EXISTS idx_person_identities_video_uuid
ON person_identities(video_uuid);
CREATE INDEX IF NOT EXISTS idx_person_identities_face
ON person_identities(face_identity_id)
WHERE face_identity_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_person_identities_speaker
ON person_identities(speaker_id)
WHERE speaker_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_person_identities_name
ON person_identities(name)
WHERE name IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_person_identities_confirmed
ON person_identities(is_confirmed)
WHERE is_confirmed = TRUE;
-- Person appearances indexes
CREATE INDEX IF NOT EXISTS idx_person_appearances_person
ON person_appearances(person_id);
CREATE INDEX IF NOT EXISTS idx_person_appearances_video
ON person_appearances(video_uuid);
CREATE INDEX IF NOT EXISTS idx_person_appearances_time
ON person_appearances(video_uuid, start_time, end_time);
CREATE INDEX IF NOT EXISTS idx_person_appearances_face
ON person_appearances(face_detection_id)
WHERE face_detection_id IS NOT NULL;
-- 7.4: Create function to update person appearance statistics
CREATE OR REPLACE FUNCTION update_person_appearance_stats(p_person_id VARCHAR(255))
RETURNS VOID AS $$
BEGIN
UPDATE person_identities
SET
appearance_count = (
SELECT COUNT(*)
FROM person_appearances
WHERE person_id = p_person_id
),
total_appearance_duration = (
SELECT COALESCE(SUM(duration), 0.0)
FROM person_appearances
WHERE person_id = p_person_id
),
first_appearance_time = (
SELECT MIN(start_time)
FROM person_appearances
WHERE person_id = p_person_id
),
last_appearance_time = (
SELECT MAX(end_time)
FROM person_appearances
WHERE person_id = p_person_id
),
updated_at = CURRENT_TIMESTAMP
WHERE person_id = p_person_id;
END;
$$ LANGUAGE plpgsql;
-- 7.5: Create trigger to auto-update statistics
CREATE OR REPLACE FUNCTION trigger_update_person_stats()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
PERFORM update_person_appearance_stats(NEW.person_id);
ELSIF TG_OP = 'UPDATE' THEN
PERFORM update_person_appearance_stats(NEW.person_id);
IF NEW.person_id != OLD.person_id THEN
PERFORM update_person_appearance_stats(OLD.person_id);
END IF;
ELSIF TG_OP = 'DELETE' THEN
PERFORM update_person_appearance_stats(OLD.person_id);
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Create trigger only if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'trigger_update_person_appearance_stats'
AND tgrelid = 'person_appearances'::regclass
) THEN
CREATE TRIGGER trigger_update_person_appearance_stats
AFTER INSERT OR UPDATE OR DELETE ON person_appearances
FOR EACH ROW
EXECUTE FUNCTION trigger_update_person_stats();
END IF;
END $$;
-- 7.6: Create function to find person by time overlap
CREATE OR REPLACE FUNCTION find_persons_at_time(
p_video_uuid VARCHAR(255),
p_time DOUBLE PRECISION,
p_tolerance DOUBLE PRECISION DEFAULT 0.0
)
RETURNS TABLE (
person_id VARCHAR(255),
name VARCHAR(255),
confidence DOUBLE PRECISION,
appearance_id INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT
pi.person_id,
pi.name,
pa.confidence,
pa.id AS appearance_id
FROM person_appearances pa
JOIN person_identities pi ON pa.person_id = pi.person_id
WHERE pa.video_uuid = p_video_uuid
AND pa.start_time <= p_time + p_tolerance
AND pa.end_time >= p_time - p_tolerance
ORDER BY pa.confidence DESC;
END;
$$ LANGUAGE plpgsql;
-- 7.7: Create function to find persons in time range
CREATE OR REPLACE FUNCTION find_persons_in_range(
p_video_uuid VARCHAR(255),
p_start_time DOUBLE PRECISION,
p_end_time DOUBLE PRECISION
)
RETURNS TABLE (
person_id VARCHAR(255),
name VARCHAR(255),
overlap_duration DOUBLE PRECISION,
confidence DOUBLE PRECISION
) AS $$
BEGIN
RETURN QUERY
SELECT
pi.person_id,
pi.name,
LEAST(pa.end_time, p_end_time) - GREATEST(pa.start_time, p_start_time) AS overlap_duration,
AVG(pa.confidence) AS confidence
FROM person_appearances pa
JOIN person_identities pi ON pa.person_id = pi.person_id
WHERE pa.video_uuid = p_video_uuid
AND pa.start_time < p_end_time
AND pa.end_time > p_start_time
GROUP BY pi.person_id, pi.name, pa.end_time, pa.start_time
ORDER BY overlap_duration DESC;
END;
$$ LANGUAGE plpgsql;
-- 7.8: Create function to merge person identities
CREATE OR REPLACE FUNCTION merge_person_identities(
p_target_person_id VARCHAR(255),
p_source_person_ids VARCHAR(255)[]
)
RETURNS VOID AS $$
BEGIN
-- Update all appearances to point to target person
UPDATE person_appearances
SET person_id = p_target_person_id
WHERE person_id = ANY(p_source_person_ids);
-- Delete source person identities
DELETE FROM person_identities
WHERE person_id = ANY(p_source_person_ids)
AND person_id != p_target_person_id;
-- Update target person statistics
PERFORM update_person_appearance_stats(p_target_person_id);
END;
$$ LANGUAGE plpgsql;
-- 7.9: Create function to auto-match face with speaker
CREATE OR REPLACE FUNCTION auto_match_face_speaker(
p_video_uuid VARCHAR(255),
p_threshold DOUBLE PRECISION DEFAULT 0.5
)
RETURNS TABLE (
face_id VARCHAR(255),
speaker_id VARCHAR(255),
confidence DOUBLE PRECISION,
match_count BIGINT
) AS $$
BEGIN
RETURN QUERY
-- Find face detections that overlap with ASRX segments
SELECT
fd.face_id,
seg.speaker_id,
COUNT(*)::DOUBLE PRECISION / NULLIF(COUNT(DISTINCT seg.speaker_id), 0) AS confidence,
COUNT(*) AS match_count
FROM face_detections fd
CROSS JOIN LATERAL (
SELECT
seg_data->>'speaker_id' AS speaker_id,
(seg_data->>'start')::DOUBLE PRECISION AS seg_start,
(seg_data->>'end')::DOUBLE PRECISION AS seg_end
FROM face_recognition_results frr,
jsonb_array_elements(frr.result_data->'frames') AS frame_data,
jsonb_array_elements(frame_data->'faces') AS face_data,
jsonb_array_elements(frr.result_data->'segments') AS seg_data
WHERE frr.video_uuid = p_video_uuid
AND face_data->>'face_id' = fd.face_id
) seg
WHERE fd.video_uuid = p_video_uuid
AND fd.timestamp_secs >= seg.seg_start
AND fd.timestamp_secs <= seg.seg_end
AND fd.face_id IS NOT NULL
AND seg.speaker_id IS NOT NULL
GROUP BY fd.face_id, seg.speaker_id
HAVING COUNT(*)::DOUBLE PRECISION / NULLIF(COUNT(DISTINCT seg.speaker_id), 0) >= p_threshold
ORDER BY confidence DESC;
END;
$$ LANGUAGE plpgsql;
-- 7.10: Add comments
COMMENT ON TABLE person_identities IS 'Stores person identity associations linking face and speaker identities';
COMMENT ON TABLE person_appearances IS 'Stores individual person appearance records with time ranges';
COMMENT ON FUNCTION update_person_appearance_stats IS 'Updates person identity statistics from appearances';
COMMENT ON FUNCTION find_persons_at_time IS 'Finds persons appearing at a specific time in video';
COMMENT ON FUNCTION find_persons_in_range IS 'Finds persons appearing in a time range with overlap calculation';
COMMENT ON FUNCTION merge_person_identities IS 'Merges multiple person identities into one';
COMMENT ON FUNCTION auto_match_face_speaker IS 'Automatically matches face detections with speaker segments';
-- 7.11: Create trigger for updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger only if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'update_person_identities_updated_at'
AND tgrelid = 'person_identities'::regclass
) THEN
CREATE TRIGGER update_person_identities_updated_at
BEFORE UPDATE ON person_identities
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
END IF;
END $$;

View File

@@ -0,0 +1,77 @@
-- Migration: 008_person_identity_binding.sql
-- Purpose: 建立聲紋 (Speaker ID)、人臉 (Face ID) 與真實身份 (Identity) 的綁定系統
-- Date: 2026-04-10
-- 1. 擴展 chunks 表,增加聲音與面孔的觀測值陣列
ALTER TABLE chunks
ADD COLUMN IF NOT EXISTS speaker_ids TEXT[] DEFAULT '{}', -- e.g. ['speaker_3', 'speaker_5']
ADD COLUMN IF NOT EXISTS face_ids TEXT[] DEFAULT '{}'; -- e.g. ['face_1']
-- 2. 建立真實身份表 (Talents / Persons)
-- 存儲現實世界中的人員資訊 (演員、配音員、真實人物)
CREATE TABLE IF NOT EXISTS talents (
id BIGSERIAL PRIMARY KEY,
real_name TEXT NOT NULL, -- 真實姓名 (e.g. "Tom Cruise")
actor_name TEXT, -- 藝名/別名
voice_embedding VECTOR(192), -- 聲紋參考向量 (ECAPA-TDNN)
face_embedding VECTOR(512), -- 人臉參考向量 (ArcFace)
metadata JSONB DEFAULT '{}', -- 其他屬性 (性別、年齡等)
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(real_name)
);
-- 建立向量索引
CREATE INDEX IF NOT EXISTS idx_talent_voice ON talents USING hnsw (voice_embedding vector_cosine_ops);
CREATE INDEX IF NOT EXISTS idx_talent_face ON talents USING hnsw (face_embedding vector_cosine_ops);
-- 3. 建立身份綁定映射表 (Identity Bindings)
-- 負責將機器生成的 ID (face_x, speaker_y) 映射到 talent_id
CREATE TABLE IF NOT EXISTS identity_bindings (
id BIGSERIAL PRIMARY KEY,
talent_id BIGINT REFERENCES talents(id) ON DELETE CASCADE,
-- 綁定類型與機器 ID
binding_type VARCHAR(32) NOT NULL, -- 'face' 或 'speaker'
binding_value VARCHAR(64) NOT NULL, -- e.g. "face_1", "speaker_3"
-- 綁定來源與狀態
source TEXT DEFAULT 'auto', -- 'auto' (自動聚類) 或 'manual' (人工綁定)
confidence FLOAT DEFAULT 0.0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
-- 每個機器 ID 只能綁定一個 Talent
UNIQUE(binding_type, binding_value)
);
-- 索引優化:加速由機器 ID 查找 Talent
CREATE INDEX IF NOT EXISTS idx_bindings_lookup ON identity_bindings(binding_type, binding_value);
CREATE INDEX IF NOT EXISTS idx_bindings_talent ON identity_bindings(talent_id);
-- 4. (選填) 建立角色表 (Characters) - 用於動畫/多語系場景
CREATE TABLE IF NOT EXISTS characters (
id BIGSERIAL PRIMARY KEY,
video_uuid TEXT NOT NULL,
name TEXT NOT NULL, -- 角色名 (e.g. "Batman")
language_track TEXT DEFAULT 'original', -- 語言軌道 (original, dub_zh_tw)
is_voice_only BOOLEAN DEFAULT FALSE, -- 是否為無臉角色 (旁白/AI)
metadata JSONB DEFAULT '{}',
UNIQUE(video_uuid, name, language_track)
);
-- 5. (選填) 建立飾演關係表 (Castings)
-- 定義 Talent 在特定視頻中飾演哪個 Character
CREATE TABLE IF NOT EXISTS castings (
id BIGSERIAL PRIMARY KEY,
character_id BIGINT REFERENCES characters(id) ON DELETE CASCADE,
talent_id BIGINT REFERENCES talents(id) ON DELETE CASCADE,
track_type VARCHAR(32) DEFAULT 'original', -- 對應音軌版本
role_type VARCHAR(32) DEFAULT 'both', -- 'voice', 'face', 'both'
UNIQUE(character_id, talent_id, track_type)
);
COMMENT ON TABLE talents IS '真實人物/演員/配音員資訊庫';
COMMENT ON TABLE identity_bindings IS '機器 ID (Face/Speaker) 與真實 Talent 的映射關係';
COMMENT ON TABLE characters IS '視頻中的劇中角色';
COMMENT ON TABLE castings is 'Talent 與 Character 的飾演關係';

View File

@@ -0,0 +1,66 @@
-- Phase 1: Data Preservation
-- 1. Add pose_results column to frames table
-- 2. Add GIN indexes for JSONB search on frames table
-- 3. Add GIN indexes for search optimization on existing columns
-- ============================================================
-- 1. Add pose_results column to frames table
-- ============================================================
ALTER TABLE frames
ADD COLUMN IF NOT EXISTS pose_results JSONB;
-- ============================================================
-- 2. GIN indexes for frames JSONB columns (enable JSONB search)
-- ============================================================
-- YOLO objects search: frames.yolo_objects @> '[{"class": "person"}]'
CREATE INDEX IF NOT EXISTS idx_frames_yolo_gin
ON frames USING GIN(yolo_objects);
-- OCR text search: frames.ocr_results @> '{"texts": [...]}'
CREATE INDEX IF NOT EXISTS idx_frames_ocr_gin
ON frames USING GIN(ocr_results);
-- Face results search: frames.face_results @> '{"faces": [...]}'
CREATE INDEX IF NOT EXISTS idx_frames_face_gin
ON frames USING GIN(face_results);
-- Pose results search: frames.pose_results @> '{"persons": [...]}'
CREATE INDEX IF NOT EXISTS idx_frames_pose_gin
ON frames USING GIN(pose_results);
-- ============================================================
-- 3. GIN index on chunks.content (currently exists but verify)
-- ============================================================
-- Note: idx_chunks_content_gin should already exist from earlier migrations.
-- This ensures it's present for content-based searches.
CREATE INDEX IF NOT EXISTS idx_chunks_content_gin
ON chunks USING GIN(content);
-- ============================================================
-- 4. Add text_content to ASRX trace chunks (backfill)
-- ASRX chunks stored as trace_asrx_* have text in content
-- but NULL text_content, making them invisible to BM25.
-- ============================================================
UPDATE chunks
SET text_content = content->>'text'
WHERE chunk_type = 'trace'
AND chunk_id LIKE 'trace_asrx_%'
AND text_content IS NULL
AND content ? 'text';
-- ============================================================
-- 5. Add text_content to YOLO trace chunks (backfill)
-- Concatenate object class names for BM25 search.
-- ============================================================
-- This is handled in the worker code for new imports.
-- For existing data, we can extract object names:
-- (commented out as it requires JSON array iteration)
-- UPDATE chunks
-- SET text_content = (
-- SELECT string_agg(obj->>'class', ' ')
-- FROM jsonb_array_elements(content->'objects') AS obj
-- )
-- WHERE chunk_type = 'trace'
-- AND chunk_id LIKE 'trace_yolo_%'
-- AND text_content IS NULL;

View File

@@ -0,0 +1,6 @@
-- Migration 010: Add visual_stats column to chunks table
-- This column stores pre-computed object counts (from YOLO) for the frames within the chunk.
-- Example: {"person": 150, "car": 12, "envelope": 5}
ALTER TABLE public.chunks ADD COLUMN IF NOT EXISTS visual_stats JSONB DEFAULT '{}';
ALTER TABLE dev.chunks ADD COLUMN IF NOT EXISTS visual_stats JSONB DEFAULT '{}';

View File

@@ -0,0 +1,30 @@
-- Migration 011: Create talents table
-- Stores global talent profiles (Actors, Real-world identities).
-- Create extension for vector if not exists (usually in 009 or similar)
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS talents (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
embedding VECTOR(768), -- Face feature vector
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Ensure identity_bindings references talents if needed
-- Current structure is generic: talent_id (bigint), identity_id (integer - likely person_id?), etc.
-- We will use talent_id to store the ID of the talent, and identity_id to store the ID of the person in person_identities (or we use uuid/identity_value).
-- Let's check current identity_bindings usage.
-- The columns are: talent_id, identity_id, uuid, identity_type, identity_value, binding_type, binding_value.
-- We will use:
-- talent_id: ID from talents table.
-- identity_id: ID of the row in person_identities (if we can map it) OR we rely on identity_value = person_id.
-- identity_type: 'person_id'
-- identity_value: 'Person_0'
-- binding_type: 'named'
-- Add index for faster lookups
CREATE INDEX IF NOT EXISTS idx_identity_bindings_talent_id ON identity_bindings(talent_id);
CREATE INDEX IF NOT EXISTS idx_identity_bindings_identity ON identity_bindings(identity_type, identity_value);

View File

@@ -0,0 +1,26 @@
-- 012_rename_to_identities.sql
-- Rename 'talents' table to 'identities' and 'talent_id' to 'identity_id'
-- This reflects the broader scope: identities can be actors, news subjects, family members, etc.
-- 1. Rename 'talents' table to 'identities'
ALTER TABLE public.talents RENAME TO identities;
ALTER TABLE dev.talents RENAME TO identities;
-- 2. Rename 'talent_id' column in 'identity_bindings' to 'identity_id'
-- We check if the column exists to avoid errors if already renamed
DO $$
BEGIN
-- Public schema
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE public.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
-- Dev schema
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'dev' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE dev.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
END $$;
-- 3. Create index on the new column
CREATE INDEX IF NOT EXISTS idx_identity_bindings_identity_id ON public.identity_bindings(identity_id);
CREATE INDEX IF NOT EXISTS idx_identity_bindings_identity_id ON dev.identity_bindings(identity_id);

View File

@@ -0,0 +1,24 @@
-- 013_rename_talents_to_identities.sql
-- Rename 'talents' to 'identities' to reflect broader scope (news, family, social, etc.)
-- 1. Rename 'talents' table to 'identities'
ALTER TABLE public.talents RENAME TO identities;
ALTER TABLE dev.talents RENAME TO identities;
-- 2. Rename 'talent_id' column in 'identity_bindings' to 'identity_id'
-- Note: We use dynamic SQL to avoid errors if the column is already renamed or doesn't exist
DO $$
BEGIN
-- Public schema
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE public.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
-- Dev schema
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'dev' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE dev.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
END $$;
-- 3. Add index for the new column name
CREATE INDEX IF NOT EXISTS idx_identity_bindings_identity_id ON identity_bindings(identity_id);

View File

@@ -0,0 +1,23 @@
-- 014_rename_to_identities.sql
-- Rename 'talents' table to 'identities' and 'talent_id' to 'identity_id'
-- This reflects the broader scope: identities can be actors, news subjects, family members, etc.
-- 1. Rename 'talents' table to 'identities'
ALTER TABLE public.talents RENAME TO identities;
ALTER TABLE dev.talents RENAME TO identities;
-- 2. Rename 'talent_id' column in 'identity_bindings' to 'identity_id'
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE public.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'dev' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE dev.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
END $$;
-- 3. Create index on the new column
CREATE INDEX IF NOT EXISTS idx_identity_bindings_identity_id ON public.identity_bindings(identity_id);
CREATE INDEX IF NOT EXISTS idx_identity_bindings_identity_id ON dev.identity_bindings(identity_id);

View File

@@ -0,0 +1,26 @@
-- 015_rename_to_identities.sql
-- Rename global 'talents' table to 'identities' and update foreign keys
-- This reflects the broader scope: identities can be actors, news subjects, family members, etc.
-- 1. Rename 'talents' table to 'identities'
ALTER TABLE public.talents RENAME TO identities;
ALTER TABLE dev.talents RENAME TO identities;
-- 2. Rename 'talent_id' column in 'identity_bindings' to 'identity_id'
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE public.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'dev' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE dev.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
END $$;
-- 3. Update indexes if needed
DROP INDEX IF EXISTS public.idx_identity_bindings_talent_id;
CREATE INDEX IF NOT EXISTS idx_identity_bindings_identity_id ON public.identity_bindings(identity_id);
DROP INDEX IF EXISTS dev.idx_identity_bindings_talent_id;
CREATE INDEX IF NOT EXISTS idx_identity_bindings_identity_id ON dev.identity_bindings(identity_id);

View File

@@ -0,0 +1,19 @@
-- 016_rename_talents_to_identities.sql
-- Rename 'talents' table to 'identities' and 'talent_id' to 'identity_id'
-- This reflects the broader scope: identities can be actors, news subjects, family members, etc.
-- 1. Rename 'talents' table to 'identities'
ALTER TABLE public.talents RENAME TO identities;
ALTER TABLE dev.talents RENAME TO identities;
-- 2. Rename 'talent_id' column in 'identity_bindings' to 'identity_id'
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE public.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'dev' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE dev.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
END $$;

View File

@@ -0,0 +1,19 @@
-- 016_rename_to_identities.sql
-- Rename 'talents' table to 'identities' and 'talent_id' to 'identity_id'
-- This reflects the broader scope: identities can be actors, news subjects, family members, etc.
-- 1. Rename 'talents' table to 'identities'
ALTER TABLE public.talents RENAME TO identities;
ALTER TABLE dev.talents RENAME TO identities;
-- 2. Rename 'talent_id' column in 'identity_bindings' to 'identity_id'
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE public.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'dev' AND table_name = 'identity_bindings' AND column_name = 'talent_id') THEN
ALTER TABLE dev.identity_bindings RENAME COLUMN talent_id TO identity_id;
END IF;
END $$;

View File

@@ -0,0 +1,35 @@
-- Migration: Add birth_registration JSONB field to videos table
-- Purpose: Store original registration information (MAC, User, Time, Path)
-- Date: 2026-04-27
-- Add birth_registration JSONB field
ALTER TABLE videos ADD COLUMN birth_registration JSONB;
-- Add comment
COMMENT ON COLUMN videos.birth_registration IS
'Birth registration information: original MAC address, username, timestamp, path';
-- Example birth_registration structure:
-- {
-- "registration_source": {
-- "mac_address": "a1:b2:c3:d4:e5:f6",
-- "username": "demo",
-- "timestamp": "2026-04-27T22:00:00+08:00",
-- "original_path": "./demo",
-- "original_filename": "GOPR0001.mp4"
-- },
-- "permission_control": {
-- "mac_binding": {
-- "license_key": "demo_license",
-- "is_active": true
-- },
-- "user_privacy": {
-- "privacy_level": "private",
-- "data_isolation": true
-- }
-- }
-- }
-- Create GIN index for JSONB queries
CREATE INDEX IF NOT EXISTS idx_videos_birth_registration
ON videos USING gin (birth_registration);

View File

@@ -0,0 +1,55 @@
-- Migration: Create mac_allocations table for MAC-based resource allocation
-- Purpose: MAC address binding for license and resource control
-- Date: 2026-04-27
-- Create mac_allocations table (simplified version for MVP)
CREATE TABLE IF NOT EXISTS mac_allocations (
mac_address VARCHAR(17) PRIMARY KEY,
machine_name VARCHAR(100),
license_key VARCHAR(64),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Add comments
COMMENT ON TABLE mac_allocations IS
'MAC address resource allocation: license binding and machine identification';
COMMENT ON COLUMN mac_allocations.mac_address IS
'Network interface MAC address (format: a1:b2:c3:d4:e5:f6)';
COMMENT ON COLUMN mac_allocations.machine_name IS
'Human-readable machine name (e.g., MacBook-Pro)';
COMMENT ON COLUMN mac_allocations.license_key IS
'License key bound to this MAC address';
COMMENT ON COLUMN mac_allocations.is_active IS
'Whether this MAC is currently active';
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_mac_allocations_license
ON mac_allocations(license_key);
CREATE INDEX IF NOT EXISTS idx_mac_allocations_active
ON mac_allocations(is_active);
-- Insert default MAC allocation for current machine (placeholder)
-- Actual MAC address will be inserted during first registration
-- INSERT INTO mac_allocations (mac_address, machine_name, license_key, is_active)
-- VALUES ('<actual_mac>', 'MacBook-Pro', 'demo_license', true);
-- Update trigger for updated_at
CREATE OR REPLACE FUNCTION update_mac_allocations_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_mac_allocations_updated_at
BEFORE UPDATE ON mac_allocations
FOR EACH ROW
EXECUTE FUNCTION update_mac_allocations_updated_at();

View File

@@ -0,0 +1,28 @@
-- Migration: Fix identities embedding dimension
-- Date: 2026-04-28
-- Issue: identities.embedding is VECTOR(768), but InsightFace outputs 512
-- 方案 A: 修改 identities 表为 512维
ALTER TABLE dev.identities
ALTER COLUMN embedding TYPE vector(512)
USING embedding::vector(512);
-- 方案 B: 或者删除并重建
-- DROP TABLE dev.identities;
-- CREATE TABLE dev.identities (
-- id SERIAL PRIMARY KEY,
-- name VARCHAR(255) NOT NULL UNIQUE,
-- embedding VECTOR(512), -- InsightFace 512维
-- metadata JSONB DEFAULT '{}'::jsonb,
-- created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- uuid UUID DEFAULT gen_random_uuid()
-- );
-- 创建向量索引(用于相似度搜索)
CREATE INDEX IF NOT EXISTS idx_identities_embedding
ON dev.identities USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 创建向量索引注释
COMMENT ON COLUMN dev.identities.embedding IS
'InsightFace 512维 embedding (ArcFace)';

View File

@@ -0,0 +1,331 @@
-- Migration 023: Extend identities table for multi-dimensional embeddings
-- Date: 2026-04-28
-- Purpose: Add identity_type, source, status, face_embedding, voice_embedding, identity_embedding, reference_data
-- Reference: docs_v1.0/ARCHITECTURE/MOMENTRY_CORE_ARCHITECTURE_V2.md
-- Strategy: Add columns to existing table (preserve existing data)
-- ============================================
-- Part 0: Ensure uuid column exists (primary key alternative)
-- ============================================
-- public schema: add uuid if not exists
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS uuid UUID DEFAULT gen_random_uuid();
-- dev schema: uuid already exists
-- ============================================
-- Part 1: Rename embedding → face_embedding (if exists)
-- ============================================
-- public schema
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'identities'
AND column_name = 'embedding'
) THEN
-- Rename column
ALTER TABLE public.identities RENAME COLUMN embedding TO face_embedding;
-- Change dimension to 512 (if currently 768)
-- Note: We cannot easily change vector dimension, so we keep as is and will fix later
-- For now, just add comment
EXECUTE 'COMMENT ON COLUMN public.identities.face_embedding IS ''InsightFace 512-dim ArcFace embedding (or 768 legacy)''';
END IF;
END $$;
-- dev schema
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'dev'
AND table_name = 'identities'
AND column_name = 'embedding'
) THEN
-- Rename column
ALTER TABLE dev.identities RENAME COLUMN embedding TO face_embedding;
-- Comment
EXECUTE 'COMMENT ON COLUMN dev.identities.face_embedding IS ''InsightFace 512-dim ArcFace embedding''';
END IF;
END $$;
-- ============================================
-- Part 2: Add identity_type VARCHAR(30)
-- ============================================
-- public schema
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS identity_type VARCHAR(30) DEFAULT 'people';
-- dev schema
ALTER TABLE dev.identities
ADD COLUMN IF NOT EXISTS identity_type VARCHAR(30) DEFAULT 'people';
COMMENT ON COLUMN public.identities.identity_type IS
'Identity type: people, brand, object, concept, logo, symbol, scene, sound, animal, environmental';
COMMENT ON COLUMN dev.identities.identity_type IS
'Identity type: people, brand, object, concept, logo, symbol, scene, sound, animal, environmental';
-- ============================================
-- Part 3: Add source VARCHAR(20)
-- ============================================
-- public schema
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS source VARCHAR(20) DEFAULT 'manual';
-- dev schema
ALTER TABLE dev.identities
ADD COLUMN IF NOT EXISTS source VARCHAR(20) DEFAULT 'manual';
COMMENT ON COLUMN public.identities.source IS
'Identity source: manual, tmdb, agent_suggested, ai_detection';
COMMENT ON COLUMN dev.identities.source IS
'Identity source: manual, tmdb, agent_suggested, ai_detection';
-- ============================================
-- Part 4: Add status VARCHAR(20)
-- ============================================
-- public schema
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'pending';
-- dev schema
ALTER TABLE dev.identities
ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'pending';
COMMENT ON COLUMN public.identities.status IS
'Identity status: pending, confirmed, skipped';
COMMENT ON COLUMN dev.identities.status IS
'Identity status: pending, confirmed, skipped';
-- ============================================
-- Part 5: Add voice_embedding VECTOR(192)
-- ============================================
-- public schema
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS voice_embedding VECTOR(192);
-- dev schema
ALTER TABLE dev.identities
ADD COLUMN IF NOT EXISTS voice_embedding VECTOR(192);
COMMENT ON COLUMN public.identities.voice_embedding IS
'ECAPA-TDNN 192-dim voice embedding';
COMMENT ON COLUMN dev.identities.voice_embedding IS
'ECAPA-TDNN 192-dim voice embedding';
-- ============================================
-- Part 6: Add identity_embedding VECTOR(768)
-- ============================================
-- public schema
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS identity_embedding VECTOR(768);
-- dev schema
ALTER TABLE dev.identities
ADD COLUMN IF NOT EXISTS identity_embedding VECTOR(768);
COMMENT ON COLUMN public.identities.identity_embedding IS
'CLIP ViT-L/14 768-dim embedding for logo/symbol/object identity';
COMMENT ON COLUMN dev.identities.identity_embedding IS
'CLIP ViT-L/14 768-dim embedding for logo/symbol/object identity';
-- ============================================
-- Part 7: Add reference_data JSONB (1-to-many embeddings)
-- ============================================
-- public schema
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS reference_data JSONB DEFAULT '{}';
-- dev schema
ALTER TABLE dev.identities
ADD COLUMN IF NOT EXISTS reference_data JSONB DEFAULT '{}';
COMMENT ON COLUMN public.identities.reference_data IS
'JSONB: {face_embeddings[], voice_embeddings[], identity_embeddings[], sound_embeddings[], image_urls[]}';
COMMENT ON COLUMN dev.identities.reference_data IS
'JSONB: {face_embeddings[], voice_embeddings[], identity_embeddings[], sound_embeddings[], image_urls[]}';
-- ============================================
-- Part 8: Add created_at and updated_at (if not exists)
-- ============================================
-- public schema: add created_at
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
-- public schema: add updated_at
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
-- dev schema: add updated_at (created_at already exists)
ALTER TABLE dev.identities
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
-- ============================================
-- Part 9: Add TMDB integration fields
-- ============================================
-- TMDB specific fields
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS tmdb_id INTEGER;
ALTER TABLE public.identities
ADD COLUMN IF NOT EXISTS tmdb_profile TEXT;
ALTER TABLE dev.identities
ADD COLUMN IF NOT EXISTS tmdb_id INTEGER;
ALTER TABLE dev.identities
ADD COLUMN IF NOT EXISTS tmdb_profile TEXT;
COMMENT ON COLUMN public.identities.tmdb_id IS
'TMDB person ID';
COMMENT ON COLUMN dev.identities.tmdb_id IS
'TMDB person ID';
COMMENT ON COLUMN public.identities.tmdb_profile IS
'TMDB profile image URL';
COMMENT ON COLUMN dev.identities.tmdb_profile IS
'TMDB profile image URL';
-- ============================================
-- Part 10: Create vector indexes
-- ============================================
-- face_embedding index
CREATE INDEX IF NOT EXISTS idx_identities_face_embedding
ON public.identities USING ivfflat (face_embedding vector_cosine_ops)
WITH (lists = 100);
CREATE INDEX IF NOT EXISTS idx_dev_identities_face_embedding
ON dev.identities USING ivfflat (face_embedding vector_cosine_ops)
WITH (lists = 100);
-- voice_embedding index
CREATE INDEX IF NOT EXISTS idx_identities_voice_embedding
ON public.identities USING ivfflat (voice_embedding vector_cosine_ops)
WITH (lists = 50);
CREATE INDEX IF NOT EXISTS idx_dev_identities_voice_embedding
ON dev.identities USING ivfflat (voice_embedding vector_cosine_ops)
WITH (lists = 50);
-- identity_embedding index
CREATE INDEX IF NOT EXISTS idx_identities_identity_embedding
ON public.identities USING ivfflat (identity_embedding vector_cosine_ops)
WITH (lists = 100);
CREATE INDEX IF NOT EXISTS idx_dev_identities_identity_embedding
ON dev.identities USING ivfflat (identity_embedding vector_cosine_ops)
WITH (lists = 100);
-- reference_data JSONB index (GIN)
CREATE INDEX IF NOT EXISTS idx_identities_reference_data
ON public.identities USING GIN (reference_data);
CREATE INDEX IF NOT EXISTS idx_dev_identities_reference_data
ON dev.identities USING GIN (reference_data);
-- uuid index
CREATE INDEX IF NOT EXISTS idx_identities_uuid
ON public.identities (uuid);
CREATE INDEX IF NOT EXISTS idx_dev_identities_uuid
ON dev.identities (uuid);
-- ============================================
-- Part 11: Add identity_type check constraint
-- ============================================
-- Update identity_type constraint to include new types
ALTER TABLE public.identities
DROP CONSTRAINT IF EXISTS identities_identity_type_check;
ALTER TABLE public.identities
ADD CONSTRAINT identities_identity_type_check
CHECK (
identity_type IN (
'people', 'brand', 'object', 'concept', 'logo', 'symbol',
'scene', 'sound', 'animal', 'environmental'
)
);
ALTER TABLE dev.identities
DROP CONSTRAINT IF EXISTS identities_identity_type_check;
ALTER TABLE dev.identities
ADD CONSTRAINT identities_identity_type_check
CHECK (
identity_type IN (
'people', 'brand', 'object', 'concept', 'logo', 'symbol',
'scene', 'sound', 'animal', 'environmental'
)
);
-- ============================================
-- Part 12: Drop old embedding index (if exists)
-- ============================================
DROP INDEX IF EXISTS public.idx_identities_embedding;
DROP INDEX IF EXISTS dev.idx_identities_embedding;
-- ============================================
-- Verification
-- ============================================
-- Verify table structure
DO $$
DECLARE
col_count INTEGER;
BEGIN
SELECT COUNT(*) INTO col_count
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'identities'
AND column_name IN (
'uuid', 'identity_type', 'source', 'status',
'face_embedding', 'voice_embedding', 'identity_embedding',
'reference_data', 'tmdb_id', 'tmdb_profile',
'created_at', 'updated_at'
);
IF col_count < 12 THEN
RAISE NOTICE 'Migration 023: Some columns missing in public.identities (count=%, expected=12)', col_count;
ELSE
RAISE NOTICE 'Migration 023: All columns added successfully to public.identities';
END IF;
SELECT COUNT(*) INTO col_count
FROM information_schema.columns
WHERE table_schema = 'dev' AND table_name = 'identities'
AND column_name IN (
'uuid', 'identity_type', 'source', 'status',
'face_embedding', 'voice_embedding', 'identity_embedding',
'reference_data', 'tmdb_id', 'tmdb_profile',
'created_at', 'updated_at'
);
IF col_count < 12 THEN
RAISE NOTICE 'Migration 023: Some columns missing in dev.identities (count=%, expected=12)', col_count;
ELSE
RAISE NOTICE 'Migration 023: All columns added successfully to dev.identities';
END IF;
END $$;

View File

@@ -0,0 +1,66 @@
-- Migration 024: Fix face_embedding dimension (768 → 512)
-- Date: 2026-04-28
-- Purpose: Correct face_embedding dimension to match ArcFace (512-dim)
-- Issue: Migration 023 renamed embedding(768) to face_embedding, but ArcFace outputs 512-dim
-- Safety: No existing embedding data, safe to drop and recreate
-- ============================================
-- Part 1: Drop face_embedding column and recreate as 512-dim
-- ============================================
-- public schema
ALTER TABLE public.identities DROP COLUMN IF EXISTS face_embedding;
ALTER TABLE public.identities ADD COLUMN face_embedding VECTOR(512);
-- dev schema
ALTER TABLE dev.identities DROP COLUMN IF EXISTS face_embedding;
ALTER TABLE dev.identities ADD COLUMN face_embedding VECTOR(512);
-- ============================================
-- Part 2: Update comments
-- ============================================
COMMENT ON COLUMN public.identities.face_embedding IS
'InsightFace ArcFace 512-dim embedding';
COMMENT ON COLUMN dev.identities.face_embedding IS
'InsightFace ArcFace 512-dim embedding';
-- ============================================
-- Part 3: Recreate index for 512-dim
-- ============================================
-- Drop old index (if exists)
DROP INDEX IF EXISTS public.idx_identities_face_embedding;
DROP INDEX IF EXISTS dev.idx_dev_identities_face_embedding;
-- Create new index for 512-dim
CREATE INDEX idx_identities_face_embedding
ON public.identities USING ivfflat (face_embedding vector_cosine_ops)
WITH (lists = 100);
CREATE INDEX idx_dev_identities_face_embedding
ON dev.identities USING ivfflat (face_embedding vector_cosine_ops)
WITH (lists = 100);
-- ============================================
-- Verification
-- ============================================
DO $$
DECLARE
dim INTEGER;
BEGIN
-- Check public schema
SELECT character_maximum_length INTO dim
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'identities'
AND column_name = 'face_embedding';
-- Note: vector type doesn't report dimension in information_schema
-- We'll check via pg_attribute instead
RAISE NOTICE 'Migration 024: face_embedding recreated as VECTOR(512) in public.identities';
RAISE NOTICE 'Migration 024: face_embedding recreated as VECTOR(512) in dev.identities';
END $$;

View File

@@ -0,0 +1,45 @@
-- Migration: 025_rename_video_uuid_to_file_uuid.sql
-- Date: 2026-04-28
-- Version: V4.0
-- Purpose: Rename video_uuid to file_uuid for terminology consistency
-- Note: Adapted to actual dev schema structure
BEGIN;
-- 1. face_detections
ALTER TABLE face_detections
RENAME COLUMN video_uuid TO file_uuid;
DROP INDEX IF EXISTS idx_face_detections_video_uuid;
CREATE INDEX IF NOT EXISTS idx_face_detections_file_uuid ON face_detections(file_uuid);
-- 2. face_clusters
ALTER TABLE face_clusters
RENAME COLUMN video_uuid TO file_uuid;
DROP INDEX IF EXISTS idx_face_clusters_video_uuid;
CREATE INDEX IF NOT EXISTS idx_face_clusters_file_uuid ON face_clusters(file_uuid);
-- 3. person_identities
ALTER TABLE person_identities
RENAME COLUMN video_uuid TO file_uuid;
DROP INDEX IF EXISTS idx_person_identities_video_uuid;
CREATE INDEX IF NOT EXISTS idx_person_identities_file_uuid ON person_identities(file_uuid);
-- 4. person_appearances
ALTER TABLE person_appearances
RENAME COLUMN video_uuid TO file_uuid;
DROP INDEX IF EXISTS idx_person_appearances_video_uuid;
CREATE INDEX IF NOT EXISTS idx_person_appearances_file_uuid ON person_appearances(file_uuid);
-- 5. chunks (check if video_uuid exists)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'chunks' AND column_name = 'video_uuid') THEN
ALTER TABLE chunks RENAME COLUMN video_uuid TO file_uuid;
END IF;
END $$;
COMMIT;

View File

@@ -0,0 +1,54 @@
-- Migration: 026_create_file_identities_table.sql (Fixed v2)
-- Date: 2026-04-28
-- Version: V4.0
-- Purpose: Create file_identities table for N:N relationship
-- Note: Uses videos table, no timestamp in face_detections
BEGIN;
-- 1. Create file_identities table
CREATE TABLE IF NOT EXISTS file_identities (
id BIGSERIAL PRIMARY KEY,
file_uuid VARCHAR(255) NOT NULL,
identity_id BIGINT NOT NULL,
face_count INTEGER DEFAULT 0,
speaker_count INTEGER DEFAULT 0,
first_appearance DOUBLE PRECISION,
last_appearance DOUBLE PRECISION,
confidence DOUBLE PRECISION DEFAULT 0.0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_file_identities_video
FOREIGN KEY (file_uuid)
REFERENCES videos(uuid)
ON DELETE CASCADE,
CONSTRAINT fk_file_identities_identity
FOREIGN KEY (identity_id)
REFERENCES identities(id)
ON DELETE CASCADE,
CONSTRAINT uq_file_identities
UNIQUE (file_uuid, identity_id)
);
-- 2. Create indexes
CREATE INDEX IF NOT EXISTS idx_file_identities_file_uuid ON file_identities(file_uuid);
CREATE INDEX IF NOT EXISTS idx_file_identities_identity_id ON file_identities(identity_id);
CREATE INDEX IF NOT EXISTS idx_file_identities_confidence ON file_identities(confidence DESC);
-- 3. Populate from existing face_detections (identity_id exists)
-- Note: face_detections doesn't have timestamp, skip first/last_appearance
INSERT INTO file_identities (file_uuid, identity_id, face_count, confidence)
SELECT
fd.file_uuid,
fd.identity_id,
COUNT(*) AS face_count,
AVG(fd.confidence) AS confidence
FROM face_detections fd
WHERE fd.identity_id IS NOT NULL
GROUP BY fd.file_uuid, fd.identity_id
ON CONFLICT (file_uuid, identity_id) DO NOTHING;
COMMIT;

View File

@@ -0,0 +1,32 @@
-- Migration: 027_add_identity_id_to_face_detections.sql
-- Date: 2026-04-28
-- Version: V4.0
-- Purpose: Add identity_id foreign key to face_detections for direct binding
BEGIN;
-- 1. Add identity_id column (if not exists)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'face_detections' AND column_name = 'identity_id') THEN
ALTER TABLE face_detections ADD COLUMN identity_id BIGINT;
END IF;
END $$;
-- 2. Add foreign key constraint
ALTER TABLE face_detections
DROP CONSTRAINT IF EXISTS fk_face_detections_identity,
ADD CONSTRAINT fk_face_detections_identity
FOREIGN KEY (identity_id)
REFERENCES identities(id)
ON DELETE SET NULL;
-- 3. Create index for identity queries
CREATE INDEX IF NOT EXISTS idx_face_detections_identity_id ON face_detections(identity_id)
WHERE identity_id IS NOT NULL;
-- 4. Create index for candidate queries (unregistered faces)
CREATE INDEX IF NOT EXISTS idx_face_detections_candidates ON face_detections(confidence DESC)
WHERE identity_id IS NULL;
COMMIT;

View File

@@ -0,0 +1,30 @@
-- Migration: 028_drop_person_identities_table.sql
-- Date: 2026-04-28
-- Version: V4.0
-- Purpose: Remove person_identities table (V3.x → V4.0 architecture)
BEGIN;
-- 1. Backup data (optional, uncomment if needed)
-- CREATE TABLE person_identities_backup AS SELECT * FROM person_identities;
-- 2. Drop indexes
DROP INDEX IF EXISTS idx_person_identities_file_uuid;
DROP INDEX IF EXISTS idx_person_identities_file_uuid;
-- 3. Drop table
DROP TABLE IF EXISTS person_identities CASCADE;
-- 4. Drop related tables (person_appearances)
DROP TABLE IF EXISTS person_appearances CASCADE;
-- 5. Drop related functions
DROP FUNCTION IF EXISTS get_person_timeline(p_file_uuid VARCHAR);
DROP FUNCTION IF EXISTS get_person_statistics(p_file_uuid VARCHAR);
DROP FUNCTION IF EXISTS get_person_timeline_with_chunks(p_file_uuid VARCHAR);
-- 6. Drop related triggers (if exists)
DROP TRIGGER IF EXISTS update_person_appearances_trigger ON face_detections;
DROP FUNCTION IF EXISTS update_person_appearances();
COMMIT;