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:
232
migrations/006_face_recognition_tables.sql
Normal file
232
migrations/006_face_recognition_tables.sql
Normal 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 $$;
|
||||
328
migrations/007_person_identity_tables.sql
Normal file
328
migrations/007_person_identity_tables.sql
Normal 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 $$;
|
||||
77
migrations/008_person_identity_binding.sql
Normal file
77
migrations/008_person_identity_binding.sql
Normal 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 的飾演關係';
|
||||
66
migrations/009_data_preservation_indexes.sql
Normal file
66
migrations/009_data_preservation_indexes.sql
Normal 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;
|
||||
6
migrations/010_add_chunk_visual_stats.sql
Normal file
6
migrations/010_add_chunk_visual_stats.sql
Normal 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 '{}';
|
||||
30
migrations/011_create_talents.sql
Normal file
30
migrations/011_create_talents.sql
Normal 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);
|
||||
26
migrations/012_rename_to_identities.sql
Normal file
26
migrations/012_rename_to_identities.sql
Normal 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);
|
||||
24
migrations/013_rename_talents_to_identities.sql
Normal file
24
migrations/013_rename_talents_to_identities.sql
Normal 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);
|
||||
23
migrations/014_rename_to_identities.sql
Normal file
23
migrations/014_rename_to_identities.sql
Normal 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);
|
||||
26
migrations/015_rename_to_identities.sql
Normal file
26
migrations/015_rename_to_identities.sql
Normal 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);
|
||||
19
migrations/016_rename_talents_to_identities.sql
Normal file
19
migrations/016_rename_talents_to_identities.sql
Normal 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 $$;
|
||||
19
migrations/016_rename_to_identities.sql
Normal file
19
migrations/016_rename_to_identities.sql
Normal 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 $$;
|
||||
35
migrations/019_add_birth_registration.sql
Normal file
35
migrations/019_add_birth_registration.sql
Normal 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);
|
||||
55
migrations/020_create_mac_allocations.sql
Normal file
55
migrations/020_create_mac_allocations.sql
Normal 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();
|
||||
28
migrations/021_fix_identities_embedding_dim.sql
Normal file
28
migrations/021_fix_identities_embedding_dim.sql
Normal 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)';
|
||||
331
migrations/023_extend_identities_embeddings.sql
Normal file
331
migrations/023_extend_identities_embeddings.sql
Normal 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 $$;
|
||||
66
migrations/024_fix_face_embedding_dim.sql
Normal file
66
migrations/024_fix_face_embedding_dim.sql
Normal 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 $$;
|
||||
45
migrations/025_rename_video_uuid_to_file_uuid.sql
Normal file
45
migrations/025_rename_video_uuid_to_file_uuid.sql
Normal 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;
|
||||
54
migrations/026_create_file_identities_table.sql
Normal file
54
migrations/026_create_file_identities_table.sql
Normal 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;
|
||||
32
migrations/027_add_identity_id_to_face_detections.sql
Normal file
32
migrations/027_add_identity_id_to_face_detections.sql
Normal 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;
|
||||
30
migrations/028_drop_person_identities_table.sql
Normal file
30
migrations/028_drop_person_identities_table.sql
Normal 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;
|
||||
Reference in New Issue
Block a user