Initial commit: WordPress wp-content (themes, plugins, languages)
- Theme: momentry (custom theme with REST API routes) - Plugins: code-snippets (contains all API proxies) - Languages: zh_TW translations - Excludes: cache, backups, uploads, logs
This commit is contained in:
382
themes/momentry/inc/api/class-identity-api.php
Normal file
382
themes/momentry/inc/api/class-identity-api.php
Normal file
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Momentry;
|
||||
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
use PDO;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Identity_API {
|
||||
private PDO $db;
|
||||
private string $schema;
|
||||
|
||||
public function __construct() {
|
||||
$this->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<uuid>[a-f0-9\-]{36})', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_identity_detail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route('momentry/v1', '/identities/(?P<uuid>[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<uuid>[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<uuid>[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,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user