galleria_photo_face_rknn_op.../db/init.j
2026-10-03 12:16:58 +02:00

547 lines
15 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// db/init.js
const db = require('./knex');
/* ============================================================
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");
}
}
/* ============================================================
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");
}
}
/* ============================================================
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 ensureFaceClusters();
await ensurePersons();
console.log("✅ init completato");
process.exit(0);
} catch (err) {
console.error("❌ errore init:", err);
process.exit(1);
}
}
init();