1use rusqlite::{params, OptionalExtension, Result as SqliteResult};
4
5use super::FaceRepo;
6
7impl<'a> FaceRepo<'a> {
8 pub fn reset_if_no_faces(&self) -> SqliteResult<usize> {
13 let face_count: i64 = self
14 .conn
15 .query_row("SELECT COUNT(*) FROM faces", [], |row| row.get(0))?;
16 let processed_count: i64 = self.conn.query_row(
17 "SELECT COUNT(*) FROM photos WHERE faces_processed = TRUE",
18 [],
19 |row| row.get(0),
20 )?;
21
22 if face_count == 0 && processed_count > 0 {
23 let reset = self.conn.execute(
24 "UPDATE photos SET faces_processed = FALSE WHERE faces_processed = TRUE",
25 [],
26 )?;
27 tracing::info!(
28 "Reset faces_processed flag on {} photos (no faces were actually detected)",
29 reset
30 );
31 Ok(reset)
32 } else {
33 Ok(0)
34 }
35 }
36
37 pub fn name_cluster(&self, cluster_id: i64, name: &str) -> SqliteResult<()> {
39 self.conn.execute(
40 "UPDATE face_clusters SET name = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
41 params![name, cluster_id],
42 )?;
43 Ok(())
44 }
45
46 pub fn delete_cluster(&self, cluster_id: i64) -> SqliteResult<()> {
53 let tx = self.conn.unchecked_transaction()?;
54 tx.execute(
55 "UPDATE faces SET cluster_id = NULL WHERE cluster_id = ?1",
56 params![cluster_id],
57 )?;
58 tx.execute(
59 "DELETE FROM photo_inferred_identities WHERE cluster_id = ?1",
60 params![cluster_id],
61 )?;
62 tx.execute(
63 "DELETE FROM person_gallery_embeddings WHERE cluster_id = ?1",
64 params![cluster_id],
65 )?;
66 tx.execute(
67 "DELETE FROM face_clusters WHERE id = ?1",
68 params![cluster_id],
69 )?;
70 tx.commit()
71 }
72
73 pub fn assign_face_to_cluster(&self, face_id: i64, cluster_id: i64) -> SqliteResult<()> {
75 let tx = self.conn.unchecked_transaction()?;
76
77 tx.execute(
78 "UPDATE faces SET cluster_id = ?1 WHERE id = ?2",
79 params![cluster_id, face_id],
80 )?;
81
82 Self::refresh_cluster_stats_tx(&tx, cluster_id)?;
83 Self::refresh_gallery_tx(&tx, cluster_id)?;
84
85 tx.commit()
86 }
87
88 pub fn create_cluster(&self, face_ids: &[i64]) -> SqliteResult<i64> {
90 let tx = self.conn.unchecked_transaction()?;
91
92 tx.execute(
93 r#"
94 INSERT INTO face_clusters (face_count, photo_count)
95 VALUES (0, 0)
96 "#,
97 [],
98 )?;
99
100 let cluster_id = tx.last_insert_rowid();
101
102 for face_id in face_ids {
104 tx.execute(
105 "UPDATE faces SET cluster_id = ?1 WHERE id = ?2",
106 params![cluster_id, face_id],
107 )?;
108 }
109
110 Self::refresh_cluster_stats_tx(&tx, cluster_id)?;
111 Self::refresh_gallery_tx(&tx, cluster_id)?;
112
113 tx.commit()?;
114
115 Ok(cluster_id)
116 }
117
118 pub fn merge_clusters(&self, source_id: i64, target_id: i64) -> SqliteResult<()> {
123 let blocked: i64 = self.conn.query_row(
124 r#"
125 SELECT COUNT(*) FROM cluster_cannot_merge
126 WHERE (cluster_a_id = ?1 AND cluster_b_id = ?2)
127 OR (cluster_a_id = ?2 AND cluster_b_id = ?1)
128 "#,
129 params![source_id, target_id],
130 |row| row.get(0),
131 )?;
132 if blocked > 0 {
133 tracing::warn!(
134 "Refusing to merge clusters {} and {}: user marked them different",
135 source_id,
136 target_id
137 );
138 return Ok(());
139 }
140
141 let tx = self.conn.unchecked_transaction()?;
142
143 let target_name: Option<String> = tx
146 .query_row(
147 "SELECT name FROM face_clusters WHERE id = ?1",
148 params![target_id],
149 |row| row.get(0),
150 )
151 .ok()
152 .flatten();
153 if target_name.is_none() {
154 let source_name: Option<String> = tx
155 .query_row(
156 "SELECT name FROM face_clusters WHERE id = ?1",
157 params![source_id],
158 |row| row.get(0),
159 )
160 .ok()
161 .flatten();
162 if let Some(name) = source_name {
163 tx.execute(
164 "UPDATE face_clusters SET name = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
165 params![name, target_id],
166 )?;
167 }
168 }
169
170 tx.execute(
172 "UPDATE faces SET cluster_id = ?1 WHERE cluster_id = ?2",
173 params![target_id, source_id],
174 )?;
175
176 tx.execute(
178 r#"
179 INSERT OR IGNORE INTO photo_inferred_identities (photo_id, cluster_id, source_photo_id, confidence)
180 SELECT photo_id, ?1, source_photo_id, confidence
181 FROM photo_inferred_identities
182 WHERE cluster_id = ?2
183 "#,
184 params![target_id, source_id],
185 )?;
186
187 Self::refresh_cluster_stats_tx(&tx, target_id)?;
188 Self::refresh_gallery_tx(&tx, target_id)?;
189
190 tx.execute(
192 "DELETE FROM face_clusters WHERE id = ?1",
193 params![source_id],
194 )?;
195
196 tx.commit()
197 }
198
199 pub fn get_cannot_merge_map(
201 &self,
202 ) -> SqliteResult<std::collections::HashMap<i64, std::collections::HashSet<i64>>> {
203 let mut stmt = self
204 .conn
205 .prepare("SELECT cluster_a_id, cluster_b_id FROM cluster_cannot_merge")?;
206 let rows = stmt.query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)))?;
207
208 let mut map: std::collections::HashMap<i64, std::collections::HashSet<i64>> =
209 std::collections::HashMap::new();
210 for row in rows {
211 let (a, b) = row?;
212 map.entry(a).or_default().insert(b);
213 map.entry(b).or_default().insert(a);
214 }
215 Ok(map)
216 }
217
218 pub fn enqueue_review(
223 &self,
224 face_id: i64,
225 candidate_cluster_id: i64,
226 score: f32,
227 ambiguity: Option<f32>,
228 ) -> SqliteResult<()> {
229 self.conn.execute(
230 r#"
231 INSERT INTO face_review_queue (face_id, candidate_cluster_id, score, ambiguity)
232 VALUES (?1, ?2, ?3, ?4)
233 ON CONFLICT(face_id, candidate_cluster_id) DO UPDATE SET
234 score = excluded.score,
235 ambiguity = excluded.ambiguity,
236 resolved_at = NULL,
237 resolved_as = NULL
238 "#,
239 params![face_id, candidate_cluster_id, score, ambiguity],
240 )?;
241 Ok(())
242 }
243
244 pub fn resolve_review_same(&self, queue_id: i64) -> SqliteResult<()> {
250 let tx = self.conn.unchecked_transaction()?;
251
252 let (face_id, candidate_cluster_id): (i64, i64) = tx.query_row(
253 "SELECT face_id, candidate_cluster_id FROM face_review_queue WHERE id = ?1",
254 params![queue_id],
255 |row| Ok((row.get(0)?, row.get(1)?)),
256 )?;
257
258 tx.execute(
259 "UPDATE faces SET cluster_id = ?1, user_confirmed = 1 WHERE id = ?2",
260 params![candidate_cluster_id, face_id],
261 )?;
262
263 let embedding_bytes: Vec<u8> = tx.query_row(
264 "SELECT embedding FROM faces WHERE id = ?1",
265 params![face_id],
266 |row| row.get(0),
267 )?;
268 let confidence: Option<f32> = tx
269 .query_row(
270 "SELECT confidence FROM faces WHERE id = ?1",
271 params![face_id],
272 |row| row.get(0),
273 )
274 .ok();
275
276 tx.execute(
277 r#"
278 INSERT INTO person_gallery_embeddings
279 (cluster_id, face_id, embedding, quality_score, source)
280 VALUES (?1, ?2, ?3, ?4, 'user_confirmed')
281 ON CONFLICT(cluster_id, face_id) DO UPDATE SET
282 source = 'user_confirmed',
283 quality_score = excluded.quality_score
284 "#,
285 params![
286 candidate_cluster_id,
287 face_id,
288 embedding_bytes,
289 confidence.unwrap_or(0.0),
290 ],
291 )?;
292
293 tx.execute(
294 "UPDATE face_review_queue SET resolved_at = CURRENT_TIMESTAMP, resolved_as = 'same' WHERE id = ?1",
295 params![queue_id],
296 )?;
297
298 Self::refresh_cluster_stats_tx(&tx, candidate_cluster_id)?;
299
300 tx.commit()
301 }
302
303 pub fn resolve_review_different(&self, queue_id: i64) -> SqliteResult<()> {
310 let tx = self.conn.unchecked_transaction()?;
311
312 let (face_id, candidate_cluster_id): (i64, i64) = tx.query_row(
313 "SELECT face_id, candidate_cluster_id FROM face_review_queue WHERE id = ?1",
314 params![queue_id],
315 |row| Ok((row.get(0)?, row.get(1)?)),
316 )?;
317
318 let prior_cluster: Option<i64> = tx
319 .query_row(
320 "SELECT cluster_id FROM faces WHERE id = ?1",
321 params![face_id],
322 |row| row.get(0),
323 )
324 .ok();
325
326 tx.execute(
327 "UPDATE faces SET user_confirmed = -1 WHERE id = ?1",
328 params![face_id],
329 )?;
330
331 if let Some(prior) = prior_cluster {
332 if prior != candidate_cluster_id {
333 let (a, b) = if prior < candidate_cluster_id {
334 (prior, candidate_cluster_id)
335 } else {
336 (candidate_cluster_id, prior)
337 };
338 tx.execute(
339 "INSERT OR IGNORE INTO cluster_cannot_merge (cluster_a_id, cluster_b_id) VALUES (?1, ?2)",
340 params![a, b],
341 )?;
342 }
343 }
344
345 tx.execute(
346 "UPDATE face_review_queue SET resolved_at = CURRENT_TIMESTAMP, resolved_as = 'different' WHERE id = ?1",
347 params![queue_id],
348 )?;
349
350 tx.commit()
351 }
352
353 pub fn resolve_review_skip(&self, queue_id: i64) -> SqliteResult<()> {
357 let updated = self.conn.execute(
358 "UPDATE face_review_queue SET resolved_at = CURRENT_TIMESTAMP, resolved_as = 'skipped' WHERE id = ?1",
359 params![queue_id],
360 )?;
361 if updated == 0 {
362 return Err(rusqlite::Error::QueryReturnedNoRows);
363 }
364 Ok(())
365 }
366
367 pub fn confirm_face(&self, face_id: i64) -> SqliteResult<()> {
369 let tx = self.conn.unchecked_transaction()?;
370
371 tx.execute(
372 "UPDATE faces SET user_confirmed = 1 WHERE id = ?1",
373 params![face_id],
374 )?;
375
376 let (cluster_id, embedding_bytes, confidence): (i64, Vec<u8>, Option<f32>) = tx.query_row(
377 "SELECT cluster_id, embedding, confidence FROM faces WHERE id = ?1",
378 params![face_id],
379 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
380 )?;
381
382 tx.execute(
383 r#"
384 INSERT INTO person_gallery_embeddings
385 (cluster_id, face_id, embedding, quality_score, source)
386 VALUES (?1, ?2, ?3, ?4, 'user_confirmed')
387 ON CONFLICT(cluster_id, face_id) DO UPDATE SET
388 source = 'user_confirmed',
389 quality_score = excluded.quality_score
390 "#,
391 params![
392 cluster_id,
393 face_id,
394 embedding_bytes,
395 confidence.unwrap_or(0.0)
396 ],
397 )?;
398
399 Self::refresh_cluster_stats_tx(&tx, cluster_id)?;
400 Self::refresh_gallery_tx(&tx, cluster_id)?;
401
402 tx.commit()
403 }
404
405 pub fn reject_face_to_unknown(&self, face_id: i64, prev_cluster_id: i64) -> SqliteResult<()> {
407 let tx = self.conn.unchecked_transaction()?;
408
409 tx.execute(
410 "INSERT OR IGNORE INTO face_negatives (face_id, not_cluster_id) VALUES (?1, ?2)",
411 params![face_id, prev_cluster_id],
412 )?;
413
414 tx.execute(
415 "UPDATE faces SET cluster_id = NULL, user_confirmed = 0 WHERE id = ?1",
416 params![face_id],
417 )?;
418
419 Self::refresh_cluster_stats_tx(&tx, prev_cluster_id)?;
420 Self::refresh_gallery_tx(&tx, prev_cluster_id)?;
421
422 tx.commit()
423 }
424
425 pub fn hide_face(&self, face_id: i64) -> SqliteResult<()> {
427 let tx = self.conn.unchecked_transaction()?;
428
429 let cluster_id: Option<i64> = tx
430 .query_row(
431 "SELECT cluster_id FROM faces WHERE id = ?1",
432 params![face_id],
433 |row| row.get(0),
434 )
435 .optional()?
436 .ok_or(rusqlite::Error::QueryReturnedNoRows)?;
437
438 tx.execute(
439 "UPDATE faces SET cluster_id = NULL, user_confirmed = -1 WHERE id = ?1",
440 params![face_id],
441 )?;
442
443 if let Some(cid) = cluster_id {
444 Self::refresh_cluster_stats_tx(&tx, cid)?;
445 Self::refresh_gallery_tx(&tx, cid)?;
446 }
447
448 tx.commit()
449 }
450
451 pub fn reassign_face(
454 &self,
455 face_id: i64,
456 new_cluster_id: i64,
457 old_cluster_id: i64,
458 ) -> SqliteResult<()> {
459 let tx = self.conn.unchecked_transaction()?;
460
461 tx.execute(
462 "INSERT OR IGNORE INTO face_negatives (face_id, not_cluster_id) VALUES (?1, ?2)",
463 params![face_id, old_cluster_id],
464 )?;
465
466 tx.execute(
467 "UPDATE faces SET cluster_id = ?1, user_confirmed = 1 WHERE id = ?2",
468 params![new_cluster_id, face_id],
469 )?;
470
471 let embedding_bytes: Vec<u8> = tx.query_row(
472 "SELECT embedding FROM faces WHERE id = ?1",
473 params![face_id],
474 |row| row.get(0),
475 )?;
476 let confidence: Option<f32> = tx
477 .query_row(
478 "SELECT confidence FROM faces WHERE id = ?1",
479 params![face_id],
480 |row| row.get(0),
481 )
482 .ok();
483
484 tx.execute(
485 r#"
486 INSERT INTO person_gallery_embeddings
487 (cluster_id, face_id, embedding, quality_score, source)
488 VALUES (?1, ?2, ?3, ?4, 'user_confirmed')
489 ON CONFLICT(cluster_id, face_id) DO UPDATE SET
490 source = 'user_confirmed',
491 quality_score = excluded.quality_score
492 "#,
493 params![
494 new_cluster_id,
495 face_id,
496 embedding_bytes,
497 confidence.unwrap_or(0.0)
498 ],
499 )?;
500
501 Self::refresh_cluster_stats_tx(&tx, old_cluster_id)?;
502 Self::refresh_gallery_tx(&tx, old_cluster_id)?;
503 Self::refresh_cluster_stats_tx(&tx, new_cluster_id)?;
504 Self::refresh_gallery_tx(&tx, new_cluster_id)?;
505
506 tx.commit()
507 }
508
509 pub fn confirm_face_to_cluster(&self, face_id: i64, cluster_id: i64) -> SqliteResult<()> {
511 let tx = self.conn.unchecked_transaction()?;
512
513 tx.execute(
514 "UPDATE faces SET cluster_id = ?1, user_confirmed = 1 WHERE id = ?2",
515 params![cluster_id, face_id],
516 )?;
517
518 let embedding_bytes: Vec<u8> = tx.query_row(
519 "SELECT embedding FROM faces WHERE id = ?1",
520 params![face_id],
521 |row| row.get(0),
522 )?;
523 let confidence: Option<f32> = tx
524 .query_row(
525 "SELECT confidence FROM faces WHERE id = ?1",
526 params![face_id],
527 |row| row.get(0),
528 )
529 .ok();
530
531 tx.execute(
532 r#"
533 INSERT INTO person_gallery_embeddings
534 (cluster_id, face_id, embedding, quality_score, source)
535 VALUES (?1, ?2, ?3, ?4, 'user_confirmed')
536 ON CONFLICT(cluster_id, face_id) DO UPDATE SET
537 source = 'user_confirmed',
538 quality_score = excluded.quality_score
539 "#,
540 params![
541 cluster_id,
542 face_id,
543 embedding_bytes,
544 confidence.unwrap_or(0.0)
545 ],
546 )?;
547
548 Self::refresh_cluster_stats_tx(&tx, cluster_id)?;
549 Self::refresh_gallery_tx(&tx, cluster_id)?;
550
551 tx.commit()
552 }
553
554 pub fn unresolve_review(&self, queue_id: i64) -> SqliteResult<()> {
556 let tx = self.conn.unchecked_transaction()?;
557
558 let resolved_as: Option<String> = tx
559 .query_row(
560 "SELECT resolved_as FROM face_review_queue WHERE id = ?1",
561 params![queue_id],
562 |row| row.get(0),
563 )
564 .ok();
565
566 let (face_id, candidate_cluster_id): (i64, i64) = tx.query_row(
567 "SELECT face_id, candidate_cluster_id FROM face_review_queue WHERE id = ?1",
568 params![queue_id],
569 |row| Ok((row.get(0)?, row.get(1)?)),
570 )?;
571
572 match resolved_as.as_deref() {
573 Some("same") => {
574 tx.execute(
576 "UPDATE faces SET cluster_id = NULL, user_confirmed = 0 WHERE id = ?1",
577 params![face_id],
578 )?;
579 tx.execute(
580 "DELETE FROM person_gallery_embeddings WHERE cluster_id = ?1 AND face_id = ?2 AND source = 'user_confirmed'",
581 params![candidate_cluster_id, face_id],
582 )?;
583 Self::refresh_cluster_stats_tx(&tx, candidate_cluster_id)?;
584 }
585 Some("different") => {
586 tx.execute(
587 "UPDATE faces SET user_confirmed = 0 WHERE id = ?1",
588 params![face_id],
589 )?;
590 }
593 _ => {}
594 }
595
596 tx.execute(
597 "UPDATE face_review_queue SET resolved_at = NULL, resolved_as = NULL WHERE id = ?1",
598 params![queue_id],
599 )?;
600
601 tx.commit()
602 }
603}