-- ================================================================ -- 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 $$;