1125 lines
28 KiB
Text
1125 lines
28 KiB
Text
// db/init.js
|
||
const db = require('./knex');
|
||
const {
|
||
ensureFaceScanAutomation,
|
||
} = require('./faceScanAutomation');
|
||
|
||
/* ============================================================
|
||
FOTO
|
||
============================================================ */
|
||
async function ensurePhotos() {
|
||
const exists = await db.schema.hasTable('photos');
|
||
if (!exists) {
|
||
await db.schema.createTable('photos', (t) => {
|
||
t.string('id').primary();
|
||
t.string('user');
|
||
t.string('cartella');
|
||
t.string('name');
|
||
t.string('path');
|
||
t.text('extPath').notNullable().defaultTo('');
|
||
t.text('extMedia').notNullable().defaultTo('');
|
||
t.integer('hybrid').notNullable().defaultTo(0);
|
||
t.string('thub1');
|
||
t.string('thub2');
|
||
t.string('cast');
|
||
t.string('mime_type');
|
||
t.integer('width');
|
||
t.integer('height');
|
||
t.integer('rotation');
|
||
t.integer('size_bytes');
|
||
t.integer('mtimeMs');
|
||
t.integer('duration_ms');
|
||
t.string('taken_at');
|
||
t.string('data');
|
||
t.float('lat');
|
||
t.float('lon');
|
||
t.float('alt');
|
||
t.text('location');
|
||
t.string('_indexHash');
|
||
t.string('fast_hash');
|
||
t.datetime('created_at').defaultTo(db.fn.now());
|
||
t.datetime('updated_at').defaultTo(db.fn.now());
|
||
t.datetime('deleted_at').nullable();
|
||
});
|
||
console.log("✔ photos creata");
|
||
} else {
|
||
const cols = [
|
||
'thub1','thub2','fast_hash','created_at','updated_at','deleted_at'
|
||
];
|
||
for (const col of cols) {
|
||
const existsCol = await db.schema.hasColumn('photos', col);
|
||
if (!existsCol) {
|
||
await db.schema.alterTable('photos', (t) => {
|
||
if (col === 'created_at' || col === 'updated_at')
|
||
t.datetime(col).defaultTo(db.fn.now());
|
||
else if (col === 'deleted_at')
|
||
t.datetime(col).nullable();
|
||
else
|
||
t.string(col);
|
||
});
|
||
console.log(`✔ aggiunta colonna photos.${col}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
FOTO — deleted_hard
|
||
============================================================ */
|
||
async function ensureDeletedHard() {
|
||
const exists = await db.schema.hasTable('deleted_hard');
|
||
if (!exists) {
|
||
await db.schema.createTable('deleted_hard', (t) => {
|
||
t.string('id').primary();
|
||
t.string('user');
|
||
t.datetime('deleted_at').defaultTo(db.fn.now());
|
||
});
|
||
console.log("✔ deleted_hard creata");
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
WS
|
||
============================================================ */
|
||
async function ensureWsSessions() {
|
||
const exists = await db.schema.hasTable('ws_sessions');
|
||
if (!exists) {
|
||
await db.schema.createTable('ws_sessions', (t) => {
|
||
t.string('session_id').notNullable();
|
||
t.string('device_id').notNullable();
|
||
t.string('user').notNullable();
|
||
t.integer('connected_at').notNullable();
|
||
t.integer('last_ack').notNullable();
|
||
t.integer('last_sync').nullable();
|
||
t.boolean('need_full_sync').notNullable().defaultTo(false);
|
||
|
||
t.primary(['session_id', 'device_id']);
|
||
});
|
||
console.log("✔ ws_sessions creata");
|
||
} else {
|
||
const cols = [
|
||
{ name: 'device_id', type: 'string' },
|
||
{ name: 'last_sync', type: 'integer' }
|
||
];
|
||
|
||
for (const c of cols) {
|
||
const existsCol = await db.schema.hasColumn('ws_sessions', c.name);
|
||
if (!existsCol) {
|
||
await db.schema.alterTable('ws_sessions', (t) => {
|
||
if (c.type === 'string') t.string(c.name).notNullable().defaultTo('default');
|
||
if (c.type === 'integer') t.integer(c.name).nullable();
|
||
});
|
||
console.log(`✔ aggiunta colonna ws_sessions.${c.name}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async function ensureWsPending() {
|
||
const exists = await db.schema.hasTable('ws_pending_events');
|
||
if (!exists) {
|
||
await db.schema.createTable('ws_pending_events', (t) => {
|
||
t.string('event_id').primary();
|
||
t.string('session_id').notNullable();
|
||
t.text('payload').notNullable();
|
||
t.integer('sent_at').notNullable();
|
||
t.integer('retries').notNullable().defaultTo(0);
|
||
});
|
||
console.log("✔ ws_pending_events creata");
|
||
}
|
||
}
|
||
|
||
async function ensurePendingEvents() {
|
||
const exists = await db.schema.hasTable('pending_events');
|
||
if (!exists) {
|
||
await db.schema.createTable('pending_events', (t) => {
|
||
t.increments('id').primary();
|
||
t.string('user_id').notNullable();
|
||
t.string('session_id').notNullable();
|
||
t.string('device_id').notNullable();
|
||
t.string('event_id').notNullable();
|
||
t.text('payload').notNullable();
|
||
t.integer('created_at').notNullable();
|
||
|
||
t.unique(['user_id', 'session_id', 'device_id', 'event_id']);
|
||
});
|
||
console.log("✔ pending_events creata");
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
🏃 ATTIVITÀ — nuova tabella
|
||
============================================================ */
|
||
async function ensureActivities() {
|
||
const exists = await db.schema.hasTable('activities');
|
||
if (!exists) {
|
||
await db.schema.createTable('activities', (t) => {
|
||
t.string('id').primary();
|
||
t.string('user');
|
||
t.string('cartella');
|
||
|
||
t.string('file_name');
|
||
t.string('file_extension');
|
||
t.string('format');
|
||
t.string('display_name');
|
||
t.string('route_name');
|
||
t.string('title');
|
||
t.string('description');
|
||
t.string('notes');
|
||
t.string('sport');
|
||
t.string('activity_type');
|
||
t.string('device_manufacturer');
|
||
t.string('device_model');
|
||
t.string('source_app');
|
||
|
||
t.bigInteger('start_time_millis');
|
||
t.bigInteger('end_time_millis');
|
||
t.bigInteger('duration_millis');
|
||
|
||
t.float('distance_meters');
|
||
t.float('elevation_gain_meters');
|
||
t.float('elevation_loss_meters');
|
||
|
||
t.float('min_elevation_meters');
|
||
t.float('max_elevation_meters');
|
||
|
||
t.float('file_distance_meters');
|
||
t.float('file_elevation_gain_meters');
|
||
|
||
t.float('computed_distance_meters');
|
||
t.float('computed_elevation_gain_meters');
|
||
|
||
t.float('average_speed_mps');
|
||
t.float('max_speed_mps');
|
||
t.float('average_pace_sec_per_km');
|
||
|
||
t.integer('average_heart_rate');
|
||
t.integer('max_heart_rate');
|
||
t.integer('average_cadence');
|
||
t.integer('max_cadence');
|
||
|
||
t.integer('calories');
|
||
t.integer('power_avg');
|
||
t.integer('power_max');
|
||
|
||
t.integer('point_count');
|
||
t.integer('segment_count');
|
||
t.integer('lap_count');
|
||
|
||
t.float('min_latitude');
|
||
t.float('max_latitude');
|
||
t.float('min_longitude');
|
||
t.float('max_longitude');
|
||
t.float('center_latitude');
|
||
t.float('center_longitude');
|
||
|
||
t.bigInteger('size_bytes');
|
||
t.bigInteger('last_modified_millis');
|
||
t.bigInteger('date_added_millis');
|
||
t.bigInteger('date_modified_millis');
|
||
t.bigInteger('last_scan_millis');
|
||
|
||
t.boolean('is_favorite');
|
||
t.boolean('is_hidden');
|
||
t.integer('rating');
|
||
|
||
t.text('tags');
|
||
t.text('track_preview');
|
||
t.string('scan_status');
|
||
t.text('scan_error');
|
||
t.string('path');
|
||
t.text('extPath').notNullable().defaultTo('');
|
||
t.integer('hybrid').notNullable().defaultTo(0);
|
||
t.text('diagnostics');
|
||
t.text('location');
|
||
t.string('fast_hash');
|
||
t.datetime('deleted_at').nullable();
|
||
t.datetime('created_at').defaultTo(db.fn.now());
|
||
t.datetime('updated_at').defaultTo(db.fn.now());
|
||
});
|
||
console.log("✔ activities creata");
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
ATTIVITÀ — deleted_hard
|
||
============================================================ */
|
||
async function ensureDeletedHardActivities() {
|
||
const exists = await db.schema.hasTable('deleted_hard_activities');
|
||
if (!exists) {
|
||
await db.schema.createTable('deleted_hard_activities', (t) => {
|
||
t.string('id').primary();
|
||
t.string('user');
|
||
t.datetime('deleted_at').defaultTo(db.fn.now());
|
||
});
|
||
console.log("✔ deleted_hard_activities creata");
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
ATTIVITÀ — sync_done
|
||
============================================================ */
|
||
async function ensureSyncDoneActivities() {
|
||
const exists = await db.schema.hasTable('sync_done_activities');
|
||
if (!exists) {
|
||
await db.schema.createTable('sync_done_activities', (t) => {
|
||
t.string('session_id');
|
||
t.string('device_id');
|
||
t.bigInteger('last_sync');
|
||
});
|
||
console.log("✔ sync_done_activities creata");
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
VOLTI
|
||
============================================================ */
|
||
async function ensureFaces() {
|
||
const exists = await db.schema.hasTable('faces');
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable('faces', (t) => {
|
||
// ID univoco del volto
|
||
t.string('id').primary();
|
||
|
||
// Foto da cui è stato estratto
|
||
t.string('photo_id').notNullable();
|
||
t.integer('face_index').notNullable();
|
||
|
||
// Bounding box nel frame originale
|
||
t.float('x').notNullable();
|
||
t.float('y').notNullable();
|
||
t.float('width').notNullable();
|
||
t.float('height').notNullable();
|
||
|
||
// Confidence del detector SCRFD
|
||
t.float('score').notNullable();
|
||
|
||
// 5 landmark × 2 coordinate
|
||
t.float('landmark_0_x');
|
||
t.float('landmark_0_y');
|
||
|
||
t.float('landmark_1_x');
|
||
t.float('landmark_1_y');
|
||
|
||
t.float('landmark_2_x');
|
||
t.float('landmark_2_y');
|
||
|
||
t.float('landmark_3_x');
|
||
t.float('landmark_3_y');
|
||
|
||
t.float('landmark_4_x');
|
||
t.float('landmark_4_y');
|
||
|
||
// 128 × float32 = 512 byte
|
||
t.binary('embedding').notNullable();
|
||
|
||
// Risultato HDBSCAN.
|
||
// NULL = non ancora clusterizzato.
|
||
t.string('cluster_id').nullable();
|
||
|
||
// Persona assegnata dall'utente.
|
||
// NULL = identità non ancora assegnata.
|
||
t.string('person_id').nullable();
|
||
|
||
// Stato applicativo del volto.
|
||
// detected / clustered / assigned
|
||
t.string('status').notNullable().defaultTo('detected');
|
||
|
||
t.datetime('created_at').defaultTo(db.fn.now());
|
||
t.datetime('updated_at').defaultTo(db.fn.now());
|
||
|
||
// Indici principali
|
||
t.index(['photo_id']);
|
||
t.index(['cluster_id']);
|
||
t.index(['person_id']);
|
||
t.index(['status']);
|
||
|
||
t.unique(['photo_id', 'face_index']);
|
||
// Evita due record per lo stesso volto nella stessa foto
|
||
// solo se il nostro scanner assegnerà un face_index.
|
||
});
|
||
|
||
console.log("✔ faces creata");
|
||
}
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
CODA SCANSIONE VOLTI
|
||
============================================================ */
|
||
async function ensureFaceScanJobs() {
|
||
const exists =
|
||
await db.schema.hasTable('face_scan_jobs');
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable(
|
||
'face_scan_jobs',
|
||
(t) => {
|
||
t.string('photo_id').primary();
|
||
|
||
// pending / processing / done / error
|
||
t.string('status')
|
||
.notNullable()
|
||
.defaultTo('pending');
|
||
|
||
t.integer('attempts')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
|
||
t.integer('rescan_requested')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
|
||
t.integer('faces_detected').nullable();
|
||
t.integer('execution_ms').nullable();
|
||
t.integer('applied_rotation').nullable();
|
||
|
||
t.text('last_error').nullable();
|
||
|
||
t.datetime('started_at').nullable();
|
||
t.datetime('completed_at').nullable();
|
||
|
||
t.datetime('created_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.datetime('updated_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.index(['status']);
|
||
t.index(['updated_at']);
|
||
},
|
||
);
|
||
|
||
console.log("✔ face_scan_jobs creata");
|
||
} else {
|
||
const hasRescanRequested =
|
||
await db.schema.hasColumn(
|
||
'face_scan_jobs',
|
||
'rescan_requested',
|
||
);
|
||
|
||
if (!hasRescanRequested) {
|
||
await db.schema.alterTable(
|
||
'face_scan_jobs',
|
||
(t) => {
|
||
t.integer('rescan_requested')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
},
|
||
);
|
||
|
||
console.log(
|
||
"✔ aggiunta face_scan_jobs.rescan_requested",
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
CLUSTER HDBSCAN
|
||
============================================================ */
|
||
async function ensureFaceClusters() {
|
||
const exists = await db.schema.hasTable('face_clusters');
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable('face_clusters', (t) => {
|
||
// ID persistente del cluster
|
||
t.string('id').primary();
|
||
|
||
// Identifica una specifica esecuzione di HDBSCAN.
|
||
// Esempio: "initial-2026-09-23T04:20:00"
|
||
t.string('run_id').notNullable();
|
||
|
||
// Label numerica prodotta da HDBSCAN.
|
||
// Può essere -1 per noise.
|
||
t.integer('label').notNullable();
|
||
|
||
// Numero di facce nel cluster
|
||
t.integer('size').notNullable().defaultTo(0);
|
||
|
||
// Parametri principali usati per generare il cluster
|
||
t.integer('min_samples');
|
||
t.integer('min_cluster_size');
|
||
|
||
// NULL se il cluster non è ancora stato
|
||
// associato ad una persona.
|
||
t.string('person_id').nullable();
|
||
|
||
t.datetime('created_at').defaultTo(db.fn.now());
|
||
t.datetime('updated_at').defaultTo(db.fn.now());
|
||
|
||
t.index(['run_id']);
|
||
t.index(['person_id']);
|
||
|
||
// Un label può comparire una sola volta
|
||
// all'interno dello stesso run.
|
||
t.unique(['run_id', 'label']);
|
||
});
|
||
|
||
console.log("✔ face_clusters creata");
|
||
}
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
RUN CLUSTERING VOLTI
|
||
============================================================ */
|
||
async function ensureFaceClusterRuns() {
|
||
const exists =
|
||
await db.schema.hasTable(
|
||
'face_cluster_runs',
|
||
);
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable(
|
||
'face_cluster_runs',
|
||
(t) => {
|
||
// Identificativo stabile della run.
|
||
t.string('id').primary();
|
||
|
||
// Proprietario della libreria fotografica.
|
||
t.string('user')
|
||
.notNullable();
|
||
|
||
// pending / processing / done / error
|
||
t.string('status')
|
||
.notNullable()
|
||
.defaultTo('pending');
|
||
|
||
// all / unclustered / unknown
|
||
t.string('scope')
|
||
.notNullable()
|
||
.defaultTo('all');
|
||
|
||
t.integer('embedding_count')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
|
||
t.integer('min_cluster_size')
|
||
.notNullable();
|
||
|
||
t.integer('min_samples')
|
||
.notNullable();
|
||
|
||
t.string('metric')
|
||
.notNullable()
|
||
.defaultTo('euclidean');
|
||
|
||
// eom / leaf
|
||
t.string('selection_method')
|
||
.notNullable()
|
||
.defaultTo('eom');
|
||
|
||
// sklearn / native / opencl-native
|
||
t.string('engine')
|
||
.notNullable()
|
||
.defaultTo('sklearn');
|
||
|
||
t.string('engine_version')
|
||
.nullable();
|
||
|
||
t.integer('cluster_count')
|
||
.nullable();
|
||
|
||
t.integer('clustered_faces')
|
||
.nullable();
|
||
|
||
t.integer('noise_faces')
|
||
.nullable();
|
||
|
||
t.float('noise_percent')
|
||
.nullable();
|
||
|
||
t.text('last_error')
|
||
.nullable();
|
||
|
||
t.datetime('started_at')
|
||
.nullable();
|
||
|
||
t.datetime('completed_at')
|
||
.nullable();
|
||
|
||
t.datetime('created_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.datetime('updated_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.index(['user']);
|
||
t.index(['status']);
|
||
t.index(['user', 'created_at']);
|
||
},
|
||
);
|
||
|
||
console.log(
|
||
"✔ face_cluster_runs creata",
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
APPARTENENZE DEI VOLTI AI CLUSTER
|
||
============================================================ */
|
||
async function ensureFaceClusterMemberships() {
|
||
const exists =
|
||
await db.schema.hasTable(
|
||
'face_cluster_memberships',
|
||
);
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable(
|
||
'face_cluster_memberships',
|
||
(t) => {
|
||
// Identificativo stabile della membership.
|
||
t.string('id').primary();
|
||
|
||
// Run HDBSCAN che ha prodotto il risultato.
|
||
t.string('run_id')
|
||
.notNullable();
|
||
|
||
// NULL per i volti classificati come noise.
|
||
t.string('cluster_id')
|
||
.nullable();
|
||
|
||
t.string('face_id')
|
||
.notNullable();
|
||
|
||
// Label originale HDBSCAN.
|
||
// -1 indica noise.
|
||
t.integer('label')
|
||
.notNullable();
|
||
|
||
// Probabilità di appartenenza HDBSCAN.
|
||
t.float('probability')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
|
||
t.integer('noise_flag')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
|
||
// suggested / accepted / excluded
|
||
t.string('review_status')
|
||
.notNullable()
|
||
.defaultTo('suggested');
|
||
|
||
t.string('reviewed_by')
|
||
.nullable();
|
||
|
||
t.datetime('reviewed_at')
|
||
.nullable();
|
||
|
||
t.datetime('created_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.datetime('updated_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
// Un volto compare una sola volta per run.
|
||
t.unique([
|
||
'run_id',
|
||
'face_id',
|
||
]);
|
||
|
||
t.index(['run_id']);
|
||
t.index(['cluster_id']);
|
||
t.index(['face_id']);
|
||
t.index(['review_status']);
|
||
t.index([
|
||
'run_id',
|
||
'label',
|
||
]);
|
||
},
|
||
);
|
||
|
||
console.log(
|
||
"✔ face_cluster_memberships creata",
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
ESCLUSIONI MANUALI PERSONA-VOLTO
|
||
============================================================ */
|
||
async function ensurePersonFaceExclusions() {
|
||
const exists =
|
||
await db.schema.hasTable(
|
||
'person_face_exclusions',
|
||
);
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable(
|
||
'person_face_exclusions',
|
||
(t) => {
|
||
// Persona dalla quale il volto è stato escluso.
|
||
t.string('person_id')
|
||
.notNullable();
|
||
|
||
t.string('face_id')
|
||
.notNullable();
|
||
|
||
// Proprietario della libreria.
|
||
t.string('user')
|
||
.notNullable();
|
||
|
||
t.text('reason')
|
||
.nullable();
|
||
|
||
t.string('created_by')
|
||
.nullable();
|
||
|
||
t.datetime('created_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.primary([
|
||
'person_id',
|
||
'face_id',
|
||
]);
|
||
|
||
t.index(['user']);
|
||
t.index(['face_id']);
|
||
},
|
||
);
|
||
|
||
console.log(
|
||
"✔ person_face_exclusions creata",
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
PERSONE
|
||
============================================================ */
|
||
async function ensurePersons() {
|
||
const exists = await db.schema.hasTable('persons');
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable('persons', (t) => {
|
||
// ID stabile dell'identità applicativa
|
||
t.string('id').primary();
|
||
|
||
// Utente proprietario della libreria
|
||
t.string('user').nullable();
|
||
|
||
// Nome assegnato dall'utente
|
||
t.string('name').notNullable();
|
||
|
||
t.datetime('created_at').defaultTo(db.fn.now());
|
||
t.datetime('updated_at').defaultTo(db.fn.now());
|
||
|
||
t.index(['user']);
|
||
t.index(['name']);
|
||
});
|
||
|
||
console.log("✔ persons creata");
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
PROTOTYPE ATTIVI DELLE PERSONE
|
||
============================================================ */
|
||
async function ensurePersonFacePrototypes() {
|
||
const exists =
|
||
await db.schema.hasTable(
|
||
'person_face_prototypes',
|
||
);
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable(
|
||
'person_face_prototypes',
|
||
(t) => {
|
||
// Persona rappresentata dal prototype.
|
||
t.string('person_id')
|
||
.notNullable();
|
||
|
||
// Volto reale usato come riferimento.
|
||
t.string('face_id')
|
||
.notNullable();
|
||
|
||
// Proprietario della libreria.
|
||
t.string('user')
|
||
.notNullable();
|
||
|
||
// cluster / noise / manual
|
||
t.string('source_type')
|
||
.notNullable();
|
||
|
||
// cluster_medoid /
|
||
// greedy_leave_one_out_gain /
|
||
// manual
|
||
t.string('selection_method')
|
||
.notNullable();
|
||
|
||
// Ordine nel set dei prototype.
|
||
t.integer('selection_order')
|
||
.nullable();
|
||
|
||
t.float('detector_score')
|
||
.nullable();
|
||
|
||
t.float('quality_score')
|
||
.nullable();
|
||
|
||
// 1 = usato nell'indice corrente.
|
||
t.integer('active')
|
||
.notNullable()
|
||
.defaultTo(1);
|
||
|
||
// 1 = prototype protetto manualmente.
|
||
// Il ricalcolo automatico non lo disattiva.
|
||
t.integer('locked')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
|
||
// Nome o versione dell'esperimento.
|
||
t.string('experiment_name')
|
||
.nullable();
|
||
|
||
// Metriche e metadati in formato JSON.
|
||
t.text('metrics_json')
|
||
.nullable();
|
||
|
||
t.string('created_by')
|
||
.nullable();
|
||
|
||
t.datetime('created_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.datetime('updated_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.primary([
|
||
'person_id',
|
||
'face_id',
|
||
]);
|
||
|
||
t.index(['user']);
|
||
t.index(['person_id']);
|
||
t.index(['face_id']);
|
||
t.index(['active']);
|
||
t.index([
|
||
'user',
|
||
'active',
|
||
]);
|
||
},
|
||
);
|
||
|
||
console.log(
|
||
"✔ person_face_prototypes creata",
|
||
);
|
||
} else {
|
||
const hasLocked =
|
||
await db.schema.hasColumn(
|
||
'person_face_prototypes',
|
||
'locked',
|
||
);
|
||
|
||
if (!hasLocked) {
|
||
await db.schema.alterTable(
|
||
'person_face_prototypes',
|
||
(t) => {
|
||
t.integer('locked')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
},
|
||
);
|
||
|
||
console.log(
|
||
"✔ aggiunta person_face_prototypes.locked",
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
VOLTI CONFERMATI NON ADATTI COME RIFERIMENTO
|
||
============================================================ */
|
||
async function ensurePersonFaceReferenceExclusions() {
|
||
const exists =
|
||
await db.schema.hasTable(
|
||
'person_face_reference_exclusions',
|
||
);
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable(
|
||
'person_face_reference_exclusions',
|
||
(t) => {
|
||
// La persona rimane confermata.
|
||
t.string('person_id')
|
||
.notNullable();
|
||
|
||
// Il volto non deve entrare
|
||
// nell'indice di riconoscimento.
|
||
t.string('face_id')
|
||
.notNullable();
|
||
|
||
// Proprietario della libreria.
|
||
t.string('user')
|
||
.notNullable();
|
||
|
||
// Esempi:
|
||
// occlusion_extreme_pose
|
||
// blur
|
||
// face_too_small
|
||
// bad_crop
|
||
// manual
|
||
t.string('reason_code')
|
||
.notNullable();
|
||
|
||
t.text('note')
|
||
.nullable();
|
||
|
||
t.string('created_by')
|
||
.nullable();
|
||
|
||
t.datetime('created_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.datetime('updated_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.primary([
|
||
'person_id',
|
||
'face_id',
|
||
]);
|
||
|
||
t.index(['user']);
|
||
t.index(['person_id']);
|
||
t.index(['face_id']);
|
||
t.index(['reason_code']);
|
||
},
|
||
);
|
||
|
||
console.log(
|
||
"✔ person_face_reference_exclusions creata",
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
STATO DELL'INDICE DI RICONOSCIMENTO
|
||
============================================================ */
|
||
async function ensureFaceRecognitionIndexState() {
|
||
const exists =
|
||
await db.schema.hasTable(
|
||
'face_recognition_index_state',
|
||
);
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable(
|
||
'face_recognition_index_state',
|
||
(t) => {
|
||
// Una riga per proprietario
|
||
// della libreria.
|
||
t.string('user')
|
||
.primary();
|
||
|
||
// missing / dirty / building /
|
||
// ready / error
|
||
t.string('status')
|
||
.notNullable()
|
||
.defaultTo('missing');
|
||
|
||
// 1 se l'indice deve essere
|
||
// ricostruito.
|
||
t.integer('dirty')
|
||
.notNullable()
|
||
.defaultTo(1);
|
||
|
||
t.integer('version')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
|
||
t.integer('prototype_count')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
|
||
t.string('index_path')
|
||
.nullable();
|
||
|
||
t.string('metadata_path')
|
||
.nullable();
|
||
|
||
t.datetime('built_at')
|
||
.nullable();
|
||
|
||
t.text('last_error')
|
||
.nullable();
|
||
|
||
t.datetime('created_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.datetime('updated_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.index(['status']);
|
||
t.index(['dirty']);
|
||
},
|
||
);
|
||
|
||
console.log(
|
||
"✔ face_recognition_index_state creata",
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
JOB DI RICALCOLO DEI PROTOTYPE
|
||
============================================================ */
|
||
async function ensureFacePrototypeJobs() {
|
||
const exists =
|
||
await db.schema.hasTable(
|
||
'face_prototype_jobs',
|
||
);
|
||
|
||
if (!exists) {
|
||
await db.schema.createTable(
|
||
'face_prototype_jobs',
|
||
(t) => {
|
||
t.string('id')
|
||
.primary();
|
||
|
||
t.string('user')
|
||
.notNullable();
|
||
|
||
t.string('person_id')
|
||
.notNullable();
|
||
|
||
// pending / processing / done /
|
||
// error / superseded
|
||
t.string('status')
|
||
.notNullable()
|
||
.defaultTo('pending');
|
||
|
||
// Motivo dell'ultimo aggiornamento.
|
||
t.string('reason')
|
||
.notNullable();
|
||
|
||
t.string('requested_by')
|
||
.nullable();
|
||
|
||
t.datetime('requested_at')
|
||
.notNullable()
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.datetime('started_at')
|
||
.nullable();
|
||
|
||
t.datetime('completed_at')
|
||
.nullable();
|
||
|
||
t.integer('attempts')
|
||
.notNullable()
|
||
.defaultTo(0);
|
||
|
||
t.text('last_error')
|
||
.nullable();
|
||
|
||
t.text('result_json')
|
||
.nullable();
|
||
|
||
// Firma dello stato dei volti
|
||
// all'inizio dell'elaborazione.
|
||
t.string('input_signature')
|
||
.nullable();
|
||
|
||
t.datetime('created_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.datetime('updated_at')
|
||
.defaultTo(db.fn.now());
|
||
|
||
t.index(['user']);
|
||
t.index(['person_id']);
|
||
t.index(['status']);
|
||
t.index([
|
||
'user',
|
||
'status',
|
||
'requested_at',
|
||
]);
|
||
|
||
},
|
||
);
|
||
|
||
console.log(
|
||
"✔ face_prototype_jobs creata",
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
INIT
|
||
============================================================ */
|
||
/* async function init() {
|
||
try {
|
||
console.log("🔧 init DB...");
|
||
await ensurePhotos();
|
||
await ensureDeletedHard();
|
||
|
||
await ensureActivities();
|
||
await ensureDeletedHardActivities();
|
||
await ensureSyncDoneActivities();
|
||
|
||
await ensureWsSessions();
|
||
await ensureWsPending();
|
||
await ensurePendingEvents();
|
||
|
||
console.log("✅ init completato");
|
||
process.exit(0);
|
||
} catch (err) {
|
||
console.error("❌ errore init:", err);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
*/
|
||
async function init() {
|
||
try {
|
||
console.log("🔧 init DB...");
|
||
|
||
await ensurePhotos();
|
||
await ensureDeletedHard();
|
||
|
||
await ensureActivities();
|
||
await ensureDeletedHardActivities();
|
||
await ensureSyncDoneActivities();
|
||
|
||
await ensureWsSessions();
|
||
await ensureWsPending();
|
||
await ensurePendingEvents();
|
||
|
||
// Face recognition
|
||
await ensureFaces();
|
||
await ensureFaceScanJobs();
|
||
await ensureFaceScanAutomation(db);
|
||
await ensureFaceClusters();
|
||
await ensureFaceClusterRuns();
|
||
await ensureFaceClusterMemberships();
|
||
|
||
await ensurePersons();
|
||
await ensurePersonFaceExclusions();
|
||
|
||
await ensurePersonFacePrototypes();
|
||
await ensurePersonFaceReferenceExclusions();
|
||
await ensureFaceRecognitionIndexState();
|
||
await ensureFacePrototypeJobs();
|
||
|
||
console.log("✅ init completato");
|
||
process.exit(0);
|
||
} catch (err) {
|
||
console.error("❌ errore init:", err);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
|
||
init();
|