-
-
Face ID
-
+
-
-
![FaceAI]()
-
-
-
-
-
-
{{ session ? session.user.displayName : 'Sessione FaceAI' }}
-
-
-
{{ session ? session.race.name : 'Upload selfie' }}
-
-
-
{{ activeSearch ? activeSearch.status : 'ready' }}
-
-
-
-
-
-
-
{{ statusLabel }}
-
-
- {{ busyLabel }}
-
-
Match trovati: {{ activeSearch.matchCount }}
-
Reindirizzamento alla pagina legacy filtrata in corso...
-
{{ errorMessage }}
-
+
+
+
+
+
diff --git a/faceai/apps/processor/src/config.js b/faceai/apps/processor/src/config.js
index 0be8c4b9..f3572eb5 100644
--- a/faceai/apps/processor/src/config.js
+++ b/faceai/apps/processor/src/config.js
@@ -5,7 +5,6 @@ export const config = {
workerTimeoutMs: Number(process.env.FACEAI_WORKER_TIMEOUT_MS || 5 * 60 * 1000),
runtimeRoot: process.env.FACEAI_RUNTIME_ROOT || '/data/runtime',
pklRoot: process.env.FACEAI_PKL_ROOT || '/data/pkl',
- fallbackPklRoot: process.env.FACEAI_TEST_PKL_ROOT || '/data/pkl/test',
matcherBinary: process.env.FACEAI_MATCHER_BINARY || '/opt/face-recognition/face_matcher',
searchTtlSeconds: Number(process.env.FACEAI_SEARCH_TTL_SECONDS || 24 * 60 * 60),
resultTtlSeconds: Number(process.env.FACEAI_RESULT_TTL_SECONDS || 24 * 60 * 60)
diff --git a/faceai/apps/processor/src/worker-utils.js b/faceai/apps/processor/src/worker-utils.js
index 41abbbca..a8a3811c 100644
--- a/faceai/apps/processor/src/worker-utils.js
+++ b/faceai/apps/processor/src/worker-utils.js
@@ -1,34 +1,22 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { spawn } from 'node:child_process';
+import { resolveRacePklAvailability } from '../../backend/src/race-storage.js';
-async function fileExists(filePath) {
- try {
- await fs.access(filePath);
- return true;
- } catch {
- return false;
- }
-}
+export async function resolvePklPath({ raceId, raceStorage, pklRoot }) {
+ const availability = await resolveRacePklAvailability({
+ pklRoot,
+ race: {
+ id: raceId,
+ storage: raceStorage
+ }
+ });
-export async function resolvePklPath({ raceId, pklRoot, fallbackPklRoot }) {
- const preferred = path.join(pklRoot, String(raceId), 'face_encodings.pkl');
- if (await fileExists(preferred)) {
- return preferred;
+ if (!availability.available || !availability.pklPath) {
+ throw new Error(availability.message || `No PKL file available for race ${raceId}`);
}
- const flatFile = path.join(pklRoot, `${raceId}.pkl`);
- if (await fileExists(flatFile)) {
- return flatFile;
- }
-
- const fallbackEntries = await fs.readdir(fallbackPklRoot).catch(() => []);
- const fallbackFile = fallbackEntries.find((entry) => entry.toLowerCase().endsWith('.pkl'));
- if (fallbackFile) {
- return path.join(fallbackPklRoot, fallbackFile);
- }
-
- throw new Error(`No PKL file available for race ${raceId}`);
+ return availability.pklPath;
}
export async function runFaceMatcher({ matcherBinary, selfiePath, pklPath, csvPath, logPath, timeoutMs }) {
diff --git a/faceai/apps/processor/src/worker.js b/faceai/apps/processor/src/worker.js
index 28a91613..bb5af2da 100644
--- a/faceai/apps/processor/src/worker.js
+++ b/faceai/apps/processor/src/worker.js
@@ -30,8 +30,8 @@ async function processJob(job) {
try {
const pklPath = await resolvePklPath({
raceId: search.raceId,
- pklRoot: config.pklRoot,
- fallbackPklRoot: config.fallbackPklRoot
+ raceStorage: search.raceStorage,
+ pklRoot: config.pklRoot
});
const csvPath = path.join(searchDir, 'result.csv');
diff --git a/faceai/docker-compose.yml b/faceai/docker-compose.yml
index 7693b083..daa869a7 100644
--- a/faceai/docker-compose.yml
+++ b/faceai/docker-compose.yml
@@ -9,6 +9,7 @@ services:
FACEAI_FRONTEND_URL: http://localhost:3001
FACEAI_PUBLIC_BASE_URL: http://localhost:3001
FACEAI_LEGACY_RETURN_URL: http://localhost:8080/faceai_return.php
+ FACEAI_PKL_ROOT: /data/pkl
FACEAI_ENABLE_LOCAL_LEGACY_STATIC: 1
FACEAI_LOCAL_LEGACY_STATIC_ROOT: /legacy-www
FACEAI_SHARED_SECRET: change-me
@@ -19,6 +20,7 @@ services:
volumes:
- .:/app
- ../www:/legacy-www:ro
+ - ../test_pkl:/data/pkl:ro
- faceai-runtime:/data/runtime
ports:
- "3001:3001"
@@ -35,13 +37,12 @@ services:
FACEAI_QUEUE_NAME: faceai-searches
FACEAI_RUNTIME_ROOT: /data/runtime
FACEAI_PKL_ROOT: /data/pkl
- FACEAI_TEST_PKL_ROOT: /data/pkl/test
FACEAI_WORKER_CONCURRENCY: 2
FACEAI_MATCHER_BINARY: /opt/face-recognition/face_matcher
volumes:
- .:/app
- ../bin/Face_Recognition_Unix:/opt/face-recognition:ro
- - ../test_pkl:/data/pkl/test:ro
+ - ../test_pkl:/data/pkl:ro
- faceai-runtime:/data/runtime
depends_on:
- redis
@@ -55,6 +56,7 @@ services:
image: php:8.3-apache
container_name: regalami-legacy-php
environment:
+ FACEAI_FEATURE_ENABLED: 1
FACEAI_BACKEND_INTERNAL_URL: http://faceai:3001
FACEAI_FRONTEND_URL: http://localhost:3001
FACEAI_SHARED_SECRET: change-me
diff --git a/faceai/docs/processor-technical-design.md b/faceai/docs/processor-technical-design.md
index cc523665..2438b706 100644
--- a/faceai/docs/processor-technical-design.md
+++ b/faceai/docs/processor-technical-design.md
@@ -10,7 +10,7 @@ Add an internal processor service that executes `face_matcher` jobs for the publ
- add a dedicated `processor` workspace and container scaffold
- replace in-memory search orchestration in the public backend
- preserve the existing frontend polling and legacy return flow
-- support local PKL testing from `test_pkl/`
+- support local PKL testing from `test_pkl/` mounted with the same directory shape used in hosted deployment
This slice does not yet implement production NAS mounting, persistent databases, or a final parser tailored to the real matcher CSV format.
@@ -53,25 +53,34 @@ The lock is released only when the processor marks the search as terminal: `comp
## Race And PKL Resolution
-The canonical race key is the legacy `id_gara`, already exposed as `raceId` in the existing handoff flow.
+The canonical race key is still the legacy `id_gara`, but the worker no longer guesses the PKL path from `raceId` alone.
-The processor resolves the PKL path using a race-based directory layout:
+The legacy handoff must provide a `raceStorage` object with:
+
+- `year`
+- `monthFolder` like `04.APRILE`
+- `raceFolder` like `PISA`
+
+The processor resolves the PKL path using this mounted directory layout:
```text
/data/pkl/
- 101/
- face_encodings.pkl
- 202/
- face_encodings.pkl
+ 2026/
+ 04.APRILE/
+ PISA/
+ face_encodings_20260330_170210.pkl
+ LUCCA/
+ face_encodings_20260330_170155.pkl
```
The lookup rule is:
-1. try `/data/pkl/{raceId}/face_encodings.pkl`
-2. optionally fall back to `/data/pkl/{raceId}.pkl`
-3. fail the job if neither exists
+1. resolve `/data/pkl/{year}/{monthFolder}/{raceFolder}`
+2. list files at that race root
+3. take the first `.pkl` file found there, regardless of filename
+4. fail the job if the directory does not exist or contains no `.pkl` file
-For local development, `test_pkl/` is mounted into `/data/pkl/test` and the backend can fall back to the first `.pkl` file in that folder when no race-specific file exists yet.
+For local development, `test_pkl/` is mounted directly into `/data/pkl` in both the public FaceAI container and the processor container, so the same rule is used in every environment.
## Shared Runtime Storage
@@ -91,14 +100,15 @@ Both the public backend and the processor mount the same writable runtime direct
1. frontend uploads a selfie and calls `POST /api/searches`
2. backend validates session, rate limit, and active-user lock
-3. backend stores the upload and creates a Redis search record with status `queued`
-4. backend enqueues a BullMQ job
-5. processor picks up the job and sets status `processing`
-6. processor runs `face_matcher`
-7. processor parses CSV output into matches
-8. processor stores a result record and marks the search `completed`
-9. frontend polling reads Redis-backed state through `GET /api/searches/:id`
-10. existing redirect flow sends the user back to the legacy filtered page
+3. backend verifies that the mounted race directory exists and already contains a `.pkl`; if not, it rejects the request before queueing
+4. backend stores the upload and creates a Redis search record with status `queued`
+5. backend enqueues a BullMQ job
+6. processor picks up the job and sets status `processing`
+7. processor runs `face_matcher`
+8. processor parses CSV output into matches
+9. processor stores a result record and marks the search `completed`
+10. frontend polling reads Redis-backed state through `GET /api/searches/:id`
+11. existing redirect flow sends the user back to the legacy filtered page
## Search Record Shape
@@ -107,6 +117,11 @@ Both the public backend and the processor mount the same writable runtime direct
"id": "search_...",
"status": "queued",
"raceId": "101",
+ "raceStorage": {
+ "year": "2026",
+ "monthFolder": "04.APRILE",
+ "raceFolder": "PISA"
+ },
"userId": "legacy-user-1",
"returnUrl": "https://...",
"lang": "it",
@@ -162,5 +177,4 @@ Both the public backend and the processor mount the same writable runtime direct
- confirm the real CSV columns emitted by `face_matcher`
- verify the Linux binary shared library requirements inside the processor image
-- replace the PKL fallback with a strict NAS-backed race mapping once the final folder layout is agreed
- add cleanup jobs for expired runtime files
\ No newline at end of file
diff --git a/test_pkl/face_encodings_20260330_170155.pkl b/test_pkl/2026/04.APRILE/LUCCA/face_encodings_20260330_170155.pkl
similarity index 100%
rename from test_pkl/face_encodings_20260330_170155.pkl
rename to test_pkl/2026/04.APRILE/LUCCA/face_encodings_20260330_170155.pkl
diff --git a/test_pkl/2026/04.APRILE/PISA/face_encodings_20260412_171808.pkl b/test_pkl/2026/04.APRILE/PISA/face_encodings_20260412_171808.pkl
new file mode 100644
index 00000000..09628c3c
Binary files /dev/null and b/test_pkl/2026/04.APRILE/PISA/face_encodings_20260412_171808.pkl differ
diff --git a/test_pkl/face_encodings_20260330_170210.pkl b/test_pkl/face_encodings_20260330_170210.pkl
deleted file mode 100644
index f73173a1..00000000
Binary files a/test_pkl/face_encodings_20260330_170210.pkl and /dev/null differ
diff --git a/www/_js/rus-ecom-240621.js b/www/_js/rus-ecom-240621.js
index b217394d..d6f5b561 100644
--- a/www/_js/rus-ecom-240621.js
+++ b/www/_js/rus-ecom-240621.js
@@ -114,6 +114,19 @@ function getCurrentLangValue() {
return $("html").attr("lang") || "it";
}
+function getFaceAiStorageValue(fieldId, simulatorKey) {
+ var field = $("#" + fieldId);
+ if (field.length && field.val()) {
+ return field.val();
+ }
+
+ if (window.faceAiSimulator && window.faceAiSimulator.raceStorage && window.faceAiSimulator.raceStorage[simulatorKey]) {
+ return window.faceAiSimulator.raceStorage[simulatorKey];
+ }
+
+ return "";
+}
+
function faceAiFeatureEnabled() {
var config = window.faceAiConfig || {};
var simulatorConfig = window.faceAiSimulator || {};
@@ -136,6 +149,57 @@ function faceAiEscapeHtml(value) {
.replace(/'/g, "'");
}
+function isFaceAiDebugEnabled() {
+ var hostname = window.location && window.location.hostname ? window.location.hostname : "";
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
+}
+
+function getFaceAiDebugPayload() {
+ var raceYear = getFaceAiStorageValue("faceAiRaceYear", "year");
+ var raceMonthFolder = getFaceAiStorageValue("faceAiRaceMonthFolder", "monthFolder");
+ var raceFolder = getFaceAiStorageValue("faceAiRaceFolder", "raceFolder");
+ var racePathBase = $("#faceAiRacePathBase").val() || "";
+ var raceStorageRelativeDir = $("#faceAiRaceStorageRelativeDir").val() || [raceYear, raceMonthFolder, raceFolder].filter(Boolean).join("/");
+ var simulatorConfig = window.faceAiSimulator || null;
+
+ return {
+ pageUrl: window.location.href,
+ race: {
+ id: $("#id_gara").val() || "",
+ slug: $("#garaDesc").val() || "",
+ name: $("h1.my-4").last().text().replace(/\s+/g, " ").trim(),
+ lang: getCurrentLangValue(),
+ storage: {
+ year: raceYear,
+ monthFolder: raceMonthFolder,
+ raceFolder: raceFolder,
+ pathBase: racePathBase,
+ relativeDir: raceStorageRelativeDir
+ }
+ },
+ simulator: simulatorConfig,
+ handoff: {
+ url: (simulatorConfig && simulatorConfig.handoffUrl) || "faceai_handoff.php",
+ returnUrl: (simulatorConfig && simulatorConfig.returnUrl) || window.location.href
+ }
+ };
+}
+
+function logFaceAiDebug(label, extraPayload) {
+ if (!isFaceAiDebugEnabled() || !window.console || typeof window.console.groupCollapsed !== "function") {
+ return;
+ }
+
+ var payload = getFaceAiDebugPayload();
+ if (extraPayload) {
+ payload.extra = extraPayload;
+ }
+
+ window.console.groupCollapsed("[FaceAI] " + label);
+ window.console.log(payload);
+ window.console.groupEnd();
+}
+
function getFaceAiErrorState() {
if (typeof URLSearchParams === "undefined") {
return null;
@@ -191,6 +255,9 @@ function buildFaceAiLaunchUrl() {
var raceId = $("#id_gara").val() || "";
var raceSlug = $("#garaDesc").val() || "";
var raceName = $("h1.my-4").last().text().replace(/\s+/g, " ").trim();
+ var raceYear = getFaceAiStorageValue("faceAiRaceYear", "year");
+ var raceMonthFolder = getFaceAiStorageValue("faceAiRaceMonthFolder", "monthFolder");
+ var raceFolder = getFaceAiStorageValue("faceAiRaceFolder", "raceFolder");
var lang = getCurrentLangValue();
var handoffUrl = (window.faceAiSimulator && window.faceAiSimulator.handoffUrl) || "faceai_handoff.php";
var returnUrl = (window.faceAiSimulator && window.faceAiSimulator.returnUrl) || window.location.href;
@@ -198,6 +265,9 @@ function buildFaceAiLaunchUrl() {
"raceId=" + encodeURIComponent(raceId),
"raceSlug=" + encodeURIComponent(raceSlug),
"raceName=" + encodeURIComponent(raceName),
+ "raceYear=" + encodeURIComponent(raceYear),
+ "raceMonthFolder=" + encodeURIComponent(raceMonthFolder),
+ "raceFolder=" + encodeURIComponent(raceFolder),
"lang=" + encodeURIComponent(lang),
"returnUrl=" + encodeURIComponent(returnUrl)
];
@@ -215,10 +285,18 @@ function buildFaceAiLaunchUrl() {
query.push("devMembershipStatus=" + encodeURIComponent(window.faceAiSimulator.devMembershipStatus));
}
+ logFaceAiDebug("Legacy launch payload prepared", {
+ query: query.slice(0),
+ raceId: raceId,
+ raceSlug: raceSlug,
+ raceName: raceName
+ });
+
return handoffUrl + "?" + query.join("&");
}
function launchFaceAi() {
+ logFaceAiDebug("Redirecting to FaceAI handoff");
$("body").addClass("loading");
window.location.href = buildFaceAiLaunchUrl();
return false;
@@ -451,6 +529,7 @@ function goPage()
$(function() {
initFaceAiRaceSearchButton();
initFaceAiErrorModal();
+ logFaceAiDebug("Legacy race page ready");
});
diff --git a/www/faceai_handoff.php b/www/faceai_handoff.php
index 8d6231e8..466af2d2 100644
--- a/www/faceai_handoff.php
+++ b/www/faceai_handoff.php
@@ -8,6 +8,9 @@ try {
$raceId = faceai_request_value('raceId');
$raceSlug = faceai_request_value('raceSlug');
$raceName = faceai_request_value('raceName', $raceSlug !== '' ? $raceSlug : $raceId);
+ $raceYear = faceai_request_value('raceYear');
+ $raceMonthFolder = faceai_request_value('raceMonthFolder');
+ $raceFolder = faceai_request_value('raceFolder');
$lang = faceai_request_value('lang', 'it');
$returnUrl = faceai_request_value('returnUrl');
@@ -36,6 +39,20 @@ try {
faceai_redirect_with_error($returnUrl, 'Il tuo account non e abilitato all uso di Face ID.');
}
+ $racePayload = array(
+ 'id' => $raceId,
+ 'slug' => $raceSlug !== '' ? $raceSlug : $raceId,
+ 'name' => $raceName !== '' ? $raceName : $raceId
+ );
+
+ if ($raceYear !== '' && $raceMonthFolder !== '' && $raceFolder !== '') {
+ $racePayload['storage'] = array(
+ 'year' => $raceYear,
+ 'monthFolder' => $raceMonthFolder,
+ 'raceFolder' => strtoupper(trim($raceFolder))
+ );
+ }
+
$payload = array(
'type' => 'handoff',
'user' => array(
@@ -44,11 +61,7 @@ try {
'email' => $identity['email'],
'membershipStatus' => $identity['membershipStatus']
),
- 'race' => array(
- 'id' => $raceId,
- 'slug' => $raceSlug !== '' ? $raceSlug : $raceId,
- 'name' => $raceName !== '' ? $raceName : $raceId
- ),
+ 'race' => $racePayload,
'lang' => $lang,
'returnUrl' => $returnUrl,
'expiresAt' => ((int) round(microtime(true) * 1000)) + (5 * 60 * 1000)
diff --git a/www/faceai_simulator.php b/www/faceai_simulator.php
index cc027c17..90c0e20e 100644
--- a/www/faceai_simulator.php
+++ b/www/faceai_simulator.php
@@ -1,11 +1,14 @@
'f101-001', 'thumb' => 'thumb-arrivo-001.jpg', 'label' => 'Arrivo 001', 'checkpoint' => 'Arrivo'),
@@ -27,6 +30,9 @@ faceai_sim_render_page(array(
'lang' => $lang,
'raceSlug' => $raceSlug,
'raceName' => $raceName,
+ 'raceYear' => $raceYear,
+ 'raceMonthFolder' => $raceMonthFolder,
+ 'raceFolder' => $raceFolder,
'returnUrl' => $returnUrl,
'banner' => 'Questa pagina PHP simula il punto di ingresso del sito legacy. Il vecchio select con ID
tipoPuntoFoto viene rimosso dal JavaScript originale e sostituito dal pulsante Face ID.',
'totalLabel' => count($photos) . ' foto demo',
diff --git a/www/faceai_simulator_view.php b/www/faceai_simulator_view.php
index 473de53d..03cee0d5 100644
--- a/www/faceai_simulator_view.php
+++ b/www/faceai_simulator_view.php
@@ -11,6 +11,9 @@ function faceai_sim_render_page(array $options)
$lang = $options['lang'];
$raceSlug = $options['raceSlug'];
$raceName = $options['raceName'];
+ $raceYear = $options['raceYear'] ?? '';
+ $raceMonthFolder = $options['raceMonthFolder'] ?? '';
+ $raceFolder = $options['raceFolder'] ?? '';
$returnUrl = $options['returnUrl'];
$banner = $options['banner'];
$totalLabel = $options['totalLabel'];
@@ -69,8 +72,8 @@ function faceai_sim_render_page(array $options)