db = Database::get_instance(); $this->schema = Database::get_schema(); } public function register_routes(): void { register_rest_route('momentry/v1', '/identities', [ 'methods' => 'GET', 'callback' => [$this, 'get_identities'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route('momentry/v1', '/identities/(?P[a-f0-9\-]{36})', [ 'methods' => 'GET', 'callback' => [$this, 'get_identity_detail'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route('momentry/v1', '/identities/(?P[a-f0-9\-]{36})/angle-coverage', [ 'methods' => 'GET', 'callback' => [$this, 'get_angle_coverage'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route('momentry/v1', '/identities/(?P[a-f0-9\-]{36})/body-actions', [ 'methods' => 'GET', 'callback' => [$this, 'get_body_actions'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route('momentry/v1', '/identities/(?P[a-f0-9\-]{36})/reference-vectors', [ 'methods' => 'GET', 'callback' => [$this, 'get_reference_vectors'], 'permission_callback' => [$this, 'check_permission'], ]); } public function check_permission(): bool { return current_user_can('read') || $this->validate_api_key(); } private function validate_api_key(): bool { $api_key = $_SERVER['HTTP_X_MOMENTRY_API_KEY'] ?? ''; $valid_key = getenv('MOMENTRY_API_KEY'); if (!$valid_key) { return false; } return hash_equals($valid_key, $api_key); } public function get_identities(WP_REST_Request $request): WP_REST_Response { $page = (int) $request->get_param('page') ?: 1; $per_page = (int) $request->get_param('per_page') ?: 50; $offset = ($page - 1) * $per_page; $search = $request->get_param('search'); $source = $request->get_param('source'); $sql = "SELECT uuid, name, identity_type, source, tmdb_id, created_at, reference_data->>'total_references' as total_references, reference_data->>'quality_avg' as quality_avg, reference_data->'trace_stats'->>'duration_seconds' as trace_duration, reference_data->'trace_stats'->>'avg_confidence' as trace_confidence, reference_data->'trace_stats'->>'total_appearances' as trace_appearances FROM {$this->schema}.identities"; $where = []; $params = []; if ($search) { $where[] = "name ILIKE ?"; $params[] = "%{$search}%"; } if ($source) { $where[] = "source = ?"; $params[] = $source; } if (!empty($where)) { $sql .= " WHERE " . implode(' AND ', $where); } $sql .= " ORDER BY created_at DESC LIMIT ? OFFSET ?"; $params[] = $per_page; $params[] = $offset; $stmt = $this->db->prepare($sql); $stmt->execute($params); $identities = $stmt->fetchAll(PDO::FETCH_ASSOC); $count_sql = "SELECT COUNT(*) FROM {$this->schema}.identities"; if (!empty($where)) { $count_sql .= " WHERE " . implode(' AND ', $where); } $stmt = $this->db->prepare($count_sql); $stmt->execute(array_slice($params, 0, -2)); $total = (int) $stmt->fetchColumn(); $identities = array_map(function ($identity) { return $this->format_identity_list_item($identity); }, $identities); return new WP_REST_Response([ 'success' => true, 'data' => $identities, 'pagination' => [ 'page' => $page, 'per_page' => $per_page, 'total' => $total, 'total_pages' => ceil($total / $per_page), ], ]); } public function get_identity_detail(WP_REST_Request $request): WP_REST_Response|WP_Error { $uuid = $request['uuid']; $sql = "SELECT uuid, name, identity_type, source, tmdb_id, tmdb_profile, reference_data, created_at, updated_at FROM {$this->schema}.identities WHERE uuid = ?"; $stmt = $this->db->prepare($sql); $stmt->execute([$uuid]); $identity = $stmt->fetch(PDO::FETCH_ASSOC); if (!$identity) { return new WP_Error( 'not_found', 'Identity not found', ['status' => 404] ); } return new WP_REST_Response([ 'success' => true, 'data' => $this->format_identity_detail($identity), ]); } public function get_angle_coverage(WP_REST_Request $request): WP_REST_Response|WP_Error { $uuid = $request['uuid']; $sql = "SELECT reference_data FROM {$this->schema}.identities WHERE uuid = ?"; $stmt = $this->db->prepare($sql); $stmt->execute([$uuid]); $result = $stmt->fetch(PDO::FETCH_ASSOC); if (!$result) { return new WP_Error( 'not_found', 'Identity not found', ['status' => 404] ); } $reference_data = json_decode($result['reference_data'], true); $angle_coverage = $this->calculate_angle_coverage($reference_data); return new WP_REST_Response([ 'success' => true, 'data' => [ 'uuid' => $uuid, 'angles' => $angle_coverage['angles'], 'coverage_score' => $angle_coverage['score'], 'recommendation' => $angle_coverage['recommendation'], ], ]); } public function get_body_actions(WP_REST_Request $request): WP_REST_Response|WP_Error { $uuid = $request['uuid']; $sql = "SELECT i.uuid, i.name, i.reference_data FROM {$this->schema}.identities i WHERE i.uuid = ?"; $stmt = $this->db->prepare($sql); $stmt->execute([$uuid]); $identity = $stmt->fetch(PDO::FETCH_ASSOC); if (!$identity) { return new WP_Error( 'not_found', 'Identity not found', ['status' => 404] ); } $reference_data = json_decode($identity['reference_data'], true); $body_actions = $reference_data['body_actions'] ?? []; $action_statistics = $reference_data['action_statistics'] ?? []; return new WP_REST_Response([ 'success' => true, 'data' => [ 'uuid' => $uuid, 'name' => $identity['name'], 'actions' => $body_actions, 'statistics' => $action_statistics, ], ]); } public function get_reference_vectors(WP_REST_Request $request): WP_REST_Response|WP_Error { $uuid = $request['uuid']; $sql = "SELECT reference_data FROM {$this->schema}.identities WHERE uuid = ?"; $stmt = $this->db->prepare($sql); $stmt->execute([$uuid]); $result = $stmt->fetch(PDO::FETCH_ASSOC); if (!$result) { return new WP_Error( 'not_found', 'Identity not found', ['status' => 404] ); } $reference_data = json_decode($result['reference_data'], true); $face_embeddings = $reference_data['face_embeddings'] ?? []; $vectors = array_map(function ($embedding) { return [ 'angle' => $embedding['angle'] ?? 'unknown', 'frame' => $embedding['frame'] ?? null, 'quality_score' => $embedding['quality_score'] ?? 0, 'pitch' => $embedding['pitch'] ?? 'neutral', 'pose_confidence' => $embedding['pose_confidence'] ?? 0, 'detection_confidence' => $embedding['detection_confidence'] ?? 0, 'attributes' => [ 'age' => $embedding['attributes']['age'] ?? null, 'gender' => $embedding['attributes']['gender'] ?? null, ], ]; }, $face_embeddings); return new WP_REST_Response([ 'success' => true, 'data' => [ 'uuid' => $uuid, 'total_vectors' => count($vectors), 'vectors' => $vectors, ], ]); } private function format_identity_list_item(array $identity): array { return [ 'uuid' => $identity['uuid'], 'name' => $identity['name'], 'type' => $identity['identity_type'], 'source' => $identity['source'], 'total_references' => (int) ($identity['total_references'] ?? 0), 'quality_avg' => $identity['quality_avg'] ? round((float) $identity['quality_avg'], 3) : null, 'trace_duration' => $identity['trace_duration'] ? round((float) $identity['trace_duration'], 2) : null, 'trace_confidence' => $identity['trace_confidence'] ? round((float) $identity['trace_confidence'], 4) : null, 'tmdb_id' => $identity['tmdb_id'], 'created_at' => $identity['created_at'], ]; } private function format_identity_detail(array $identity): array { $reference_data = json_decode($identity['reference_data'], true); return [ 'uuid' => $identity['uuid'], 'name' => $identity['name'], 'type' => $identity['identity_type'], 'source' => $identity['source'], 'tmdb_id' => $identity['tmdb_id'], 'tmdb_profile' => $identity['tmdb_profile'], 'reference_vectors' => [ 'total' => $reference_data['total_references'] ?? 0, 'angles' => $reference_data['angles_covered'] ?? [], 'quality_avg' => $reference_data['quality_avg'] ?? null, ], 'trace_stats' => $reference_data['trace_stats'] ?? null, 'created_at' => $identity['created_at'], 'updated_at' => $identity['updated_at'], ]; } private function calculate_angle_coverage(?array $reference_data): array { if (!$reference_data) { return [ 'angles' => [], 'score' => 0, 'recommendation' => 'No reference data available', ]; } $face_embeddings = $reference_data['face_embeddings'] ?? []; $required_angles = ['frontal', 'three_quarter', 'profile_left', 'profile_right']; $angle_stats = []; foreach ($required_angles as $angle) { $angle_stats[$angle] = [ 'count' => 0, 'quality_sum' => 0, 'status' => 'missing', ]; } foreach ($face_embeddings as $embedding) { $angle = $embedding['angle'] ?? 'unknown'; $quality = $embedding['quality_score'] ?? 0; if (isset($angle_stats[$angle])) { $angle_stats[$angle]['count']++; $angle_stats[$angle]['quality_sum'] += $quality; } } $covered_count = 0; $total_quality = 0; foreach ($angle_stats as $angle => &$stats) { if ($stats['count'] > 0) { $stats['quality_avg'] = round($stats['quality_sum'] / $stats['count'], 3); $stats['status'] = $stats['count'] > 10 ? 'dominant' : 'present'; $covered_count++; $total_quality += $stats['quality_avg']; } unset($stats['quality_sum']); } $coverage_score = round(($covered_count / count($required_angles)) * 100); $avg_quality = $covered_count > 0 ? round($total_quality / $covered_count, 3) : 0; $missing_angles = array_keys(array_filter($angle_stats, fn($s) => $s['count'] === 0)); $recommendation = 'Excellent coverage!'; if (count($missing_angles) > 0) { $recommendation = 'Add ' . implode(', ', $missing_angles) . ' angle(s) for better coverage'; } return [ 'angles' => $angle_stats, 'score' => $coverage_score, 'quality_avg' => $avg_quality, 'recommendation' => $recommendation, ]; } }