# Rapport — Dédoublonnage de la médiathèque

Rédigé le 24 juillet 2026, complété le même jour après le traitement du premier lot. Conformément à
la règle finale de la demande, **aucune suppression globale automatique n'a été lancée en une seule
fois** : ce rapport documente l'audit, le système construit, la validation complète sur un groupe
unique (fusion → vérification → restauration → nouvelle fusion), puis le traitement supervisé d'un
premier lot limité de 20 groupes (voir §15). Le reste des groupes éligibles (44 restants) n'a **pas**
été traité et reste une décision à valider explicitement.

---

## 1. Définition actuelle d'un doublon — ce que signifient réellement les « 887 résultats »

Le filtre « Doublon » de la médiathèque (`whereNotNull('duplicate_of')`) affiche **887 images**,
mais ce chiffre **mélange deux choses très différentes**, posées une seule fois par
`php artisan media:scan` à l'import initial :

- Un hash cryptographique **SHA1** du fichier (`sha1`, présent sur les 4 749 médias).
- Un hash perceptuel **aHash** 8×8 (calculé mais non stocké en base), utilisé uniquement au moment
  du scan pour repérer les quasi-doublons.

Au moment du scan, si le SHA1 d'un fichier correspondait à un fichier déjà vu, **ou sinon** si son
aHash correspondait, le champ `duplicate_of` était renseigné et `confidence` passait à `'duplicate'`
— **sans distinction entre les deux cas** dans les données stockées.

**Répartition réelle des 887, reconstituée en comparant chaque `duplicate_of` à sa cible réelle :**

| | Nombre |
|---|---|
| Doublons **exacts** (SHA1 identique à leur cible historique) | 364 |
| Quasi-doublons **visuels uniquement** (aHash, SHA1 différent) | 523 |

Ce champ historique `duplicate_of` n'a cependant **pas été utilisé comme source de vérité** pour ce
dédoublonnage (voir §2) : il peut pointer vers un média depuis passé à la corbeille pour une raison
sans rapport (279 des 364 cibles historiques sont aujourd'hui en corbeille), ce qui aurait pu fausser
le choix du canonique.

## 2. Groupement autoritaire — indépendant de l'ancien système

`App\Services\MediaDeduplicationService::findExactGroups()` regroupe **à nouveau, depuis l'état réel
actuel de la base**, tous les médias actifs (hors corbeille) par `sha1` strictement identique :

- **78 groupes** de doublons exacts détectés (SHA1 identique parmi des médias actifs).
- **320 exemplaires en trop** au total dans ces groupes (à mettre à la corbeille après fusion).
- **65 groupes directement éligibles** à une fusion automatique.
- **13 groupes ambigus** (conflit de métadonnées sur un champ critique — `title` ou `event` —
  entre deux exemplaires par ailleurs identiques au niveau binaire), jamais fusionnés
  automatiquement, signalés pour revue manuelle.
- **523 quasi-doublons visuels** (aHash), affichés uniquement pour information dans
  l'administration, **jamais fusionnés automatiquement**, quel que soit le mode.

Exemple concret d'un groupe ambigu détecté (`img-01580` / `img-01638`) : deux fichiers strictement
identiques au niveau binaire, mais dont les métadonnées d'archive (`event` = « 12 » contre « 13 »,
probablement deux dossiers numérotés différents contenant par erreur la même photo) diffèrent — la
fusion automatique aurait risqué de perdre l'information de contexte correcte, d'où le signalement.

## 3. Deux niveaux de doublons — traitement différencié

- **Niveau A (exact, SHA1 identique)** : seul niveau éligible à une fusion, automatique (CLI) ou
  supervisée (interface admin).
- **Niveau B (visuel, aHash)** : jamais fusionné automatiquement. Signalé dans
  `/admin/media/duplicates` (section dédiée, lecture seule) et via le filtre existant de la
  médiathèque, pour une revue image par image.

## 4. Sélection du média canonique — règle réellement implémentée

Priorité déterministe (`MediaDeduplicationService::selectCanonical()`), dans cet ordre exact :

1. Actif dans le carrousel Hero (`hero_active`)
2. Marqué « mise en avant » (`featured`)
3. Couverture d'album (`albums.cover_media_item_id`)
4. Couverture de chronologie (`chronology_events.cover_manifest_id`)
5. Publié (`published`)
6. Nombre d'utilisations actives (`usageSummary()` : chronologie + albums + couverture + hero)
7. Richesse des métadonnées éditoriales (titre, description, légende, alt, date, année, lieu,
   évènement, source, URL source, confiance de date)
8. Résolution (largeur × hauteur)
9. Poids du fichier original
10. **Départage final uniquement** : création la plus ancienne, puis identifiant unique — jamais
    utilisé seul, uniquement quand tous les critères ci-dessus sont strictement à égalité.

Le carrousel et le statut « Hero » sont le **même signal** dans ce système (`hero_active`) : il n'y a
pas de champ distinct « carrousel » séparé du Hero, contrairement à ce qu'une lecture rapide du
cahier des charges pourrait suggérer — documenté ici explicitement.

## 5. Fusion des métadonnées — non destructive

Pour chaque champ éditorial (titre, description, légende, alt, date, année, lieu, évènement,
fonction à l'époque, libellé de catégorie, source, URL source) :

- Si le canonique est vide et la copie a une valeur → **copiée** vers le canonique.
- Si les deux ont des valeurs différentes et non vides → **conflit signalé**, valeur du canonique
  **toujours conservée**, l'autre valeur consignée dans le rapport et dans le journal d'activité
  (`old_values`/`new_values` de `MediaActivityLog`, utilisé comme note d'audit — aucune nouvelle
  colonne « notes » n'a été ajoutée pour cela, la donnée existante suffisant).
- Un conflit sur `title` ou `event` marque tout le groupe **ambigu** : fusion refusée en mode
  automatique, autorisée uniquement depuis l'administration après confirmation explicite d'un
  humain ayant vu le conflit.

Pour les indicateurs booléens (`featured`, `hero_candidate`, `curated`, `published`) : fusion par
« OU logique » — si un exemplaire quelconque du groupe portait ce statut, le canonique le conserve
après fusion (perdre un statut « mise en avant » ou « publié » porté par une copie aurait été une
perte d'information contraire à la règle finale).

Aucune donnée EXIF brute n'est stockée telle quelle dans `media_items` (seuls `date`/`year`, déjà
dérivés et protégés par la règle ci-dessus, le sont) : il n'existe donc pas de champ EXIF distinct à
perdre au-delà de ce qui est déjà protégé.

## 6. Transfert des relations

Avant chaque mise à la corbeille d'une copie :

- **Albums** : chaque association `album_media` de la copie est transférée au canonique, en
  conservant l'ordre d'affichage (`sort_order`) — **sauf** si le canonique est déjà membre du même
  album, auquel cas la ligne de la copie est simplement détachée (aucune ligne pivot dupliquée).
- **Couverture d'album** (`albums.cover_media_item_id`) : réassignée au canonique.
- **Couverture de chronologie** (`chronology_events.cover_manifest_id`) : réassignée au canonique.
- **Carrousel Hero** (`hero_active`, `hero_position`, `hero_title`, `hero_caption`, `hero_focal_x`,
  `hero_focal_y`) : si la copie était active dans le Hero et pas le canonique, l'ensemble de ces
  champs est transféré au canonique, puis désactivé sur la copie.

Chaque transfert a lieu **avant** la mise à la corbeille de la copie, dans la même transaction de
base de données que la fusion elle-même.

## 7. Sauvegarde

`App\Services\MediaBackupService` — déclenchée automatiquement et **obligatoirement** avant toute
fusion réelle (CLI `--execute` et interface admin) :

- Copie complète de `database/database.sqlite` vers
  `storage/app/backups/media-deduplication/{horodatage}/database.sqlite`.
- Export JSON + CSV des médias concernés par les groupes traités dans ce lot
  (`media-export.json` / `.csv`), avec rôle (canonique / copie à corbeille), statut, appartenance
  aux albums.
- Dossier exclu de Git (`.gitignore` mis à jour : `/storage/app/backups`, `/storage/app/reports`).
- Si la sauvegarde échoue pour une raison quelconque, **aucune fusion n'a lieu** (le mode `--execute`
  s'arrête avant de traiter le moindre groupe).

## 8. Résultats du dry-run

`php artisan media:deduplicate --dry-run` (lecture seule, aucune modification) :

```
Groupes détectés : 78
Exemplaires en trop : 320
Groupes directement éligibles : 65
Groupes ambigus (revue manuelle requise) : 13
Quasi-doublons visuels signalés : 523
```

Rapport détaillé écrit dans `storage/app/reports/media-deduplication-dry-run.json` (et `.csv`) :
pour chaque groupe, le SHA1, le canonique proposé et sa justification, la liste des copies, les
fusions de métadonnées prévues, les conflits, les transferts d'albums/couverture/chronologie/Hero,
et le statut ambigu ou non.

## 9. Groupe réellement traité (validation complète en conditions réelles)

Conformément à la consigne de ne jamais traiter les 887 (ni même les 65 éligibles) en une seule fois
lors du premier test, **un seul groupe simple, non ambigu et sans aucune relation** (pas d'album, pas
de Hero, pas de chronologie) a été traité intégralement :

- **Groupe 3** — SHA1 `07a4df47f5de27500fba57c1ee7bb3d5e5596f58`.
- Canonique : `img-01886` (`images/doc/1862376005_small_signature du livre dor.jpg`).
- Copie : `img-04087` (`images/doc/693312287_small_signature du livre dor.jpg`).
- **Vérification visuelle** des deux fichiers avant toute action : photographie identique confirmée
  (signature d'un livre d'or), cohérente avec l'identité de SHA1.

Séquence exécutée réellement sur `http://tlt.test` :

1. Sauvegarde réelle créée (`storage/app/backups/media-deduplication/2026-07-24-150256/`).
2. `php artisan media:deduplicate --execute --group=3` lancé avec confirmation interactive réelle
   (réponse « yes » saisie), pas de `--force` pour ce premier essai.
3. Fusion réussie : `img-04087` mis à la corbeille (soft delete), `img-01886` inchangé.
4. Vérifications après fusion :
   - Fichiers originaux des deux images **toujours présents sur le disque** (vérifié par `ls`).
   - Nombre total de lignes `media_items` (corbeille incluse) inchangé — aucune ligne supprimée.
   - Pages publiques (`/`, `/galerie`, `/parcours`) : toutes répondent HTTP 200 après la fusion.
   - Fiche visible dans `/admin/media/trash` (recherchée par nom de fichier via une session
     administrateur réelle).
5. **Restauration réelle** de la copie depuis `/admin/media/trash` (action HTTP réelle, pas
   simulée) : `img-04087` redevient actif, `deletion_reason` réinitialisé à `null`.
6. **Nouvelle fusion** du même groupe relancée (`--execute --group=3 --force`) : succès identique,
   nouvelle sauvegarde horodatée créée, `img-04087` de nouveau en corbeille.

Ce cycle complet (fusion → vérification multi-angle → restauration → nouvelle fusion) valide que le
système est réellement réversible et idempotent, pas seulement en théorie dans les tests
automatisés.

**État après ce test** : 431 médias en corbeille (430 avant cette intervention + 1 issu de ce
groupe). Aucune autre donnée modifiée.

## 10. Interface d'administration

Nouvelle page `/admin/media/duplicates` (lien « Doublons » ajouté au menu latéral) :

- Groupes de doublons exacts affichés par cartes : miniatures côte à côte (canonique mis en
  évidence par un cadre doré), résolution, statut (publiée/brouillon/corbeille), badges Hero/mise en
  avant par exemplaire, badge « Identique » ou « À vérifier » (groupes ambigus), raison du choix du
  canonique, conflits de métadonnées affichés en clair.
- Formulaire de fusion par groupe : sélection manuelle d'un autre canonique parmi les membres du
  groupe, confirmation obligatoire explicite pour les groupes ambigus.
- Bouton « Ignorer ce groupe » (persistant via le journal d'activité existant, action
  `dedup-group-ignored` — pas de nouvelle colonne ajoutée), avec case à cocher pour les réafficher.
- Section séparée, en lecture seule, pour les 523 quasi-doublons visuels — jamais de bouton de
  fusion à cet endroit, uniquement un lien vers la fiche de chaque image pour une revue individuelle.

## 11. Ce qui n'a volontairement PAS été fait — état après le premier lot (voir §15)

Après la validation sur un groupe unique (§9) puis le traitement du premier lot de 20 groupes
supplémentaires (§15), **44 groupes éligibles restent volontairement non traités** — décision à
valider explicitement avant toute suite, conformément à « Ne lance aucune suppression globale
automatique avant d'avoir présenté les résultats de la simulation » et à la consigne explicite de
s'arrêter après ce premier lot.

Prochaine étape possible, sur validation explicite :

```
php artisan media:deduplicate --execute --batch=20
```

(ou un `--batch` d'une autre taille) pour traiter un nouveau lot limité, suivi d'une nouvelle
vérification avant de poursuivre — ou un traitement au cas par cas depuis
`/admin/media/duplicates`.

Les 13 groupes ambigus et les 523 quasi-doublons visuels **ne seront jamais traités
automatiquement** : ils nécessitent une revue humaine au cas par cas, quel que soit le volume traité
par ailleurs.

## 12. Tests

30 tests dédiés (`tests/Feature/MediaDeduplicationTest.php`) + suite complète du projet :
**174 tests, 425 assertions, tous passants**. Couverture : détection exacte vs visuelle, sélection
canonique déterministe (Hero, mise en avant, publié, jamais simplement « le plus récent »), fusion
non destructive des métadonnées, conflit signalé, transfert albums (sans doublon pivot), couverture
d'album, couverture de chronologie, transfert Hero, soft delete jamais suppression physique,
restauration, dry-run sans modification, groupe ambigu ignoré par l'exécution automatique,
idempotence (double exécution sans effet supplémentaire), `--group=` ciblé, journal d'activité,
absence de référence orpheline (carrousel Hero toujours cohérent après fusion), pages publiques
opérationnelles après fusion, accès admin protégé, fusion via l'interface avec confirmation
explicite pour les groupes ambigus, groupe ignoré exclu de la liste par défaut.

## 13. Procédure de restauration

Trois niveaux, du plus simple au plus complet :

1. **Un seul média** : `/admin/media/trash`, rechercher par nom de fichier, cliquer « Restaurer ».
2. **Un groupe entier** : restaurer chaque copie individuellement depuis la corbeille (aucune action
   groupée de restauration n'existe, par cohérence avec le système de corbeille déjà en place).
3. **Restauration complète depuis une sauvegarde** : arrêter l'application, remplacer
   `database/database.sqlite` par la copie correspondante dans
   `storage/app/backups/media-deduplication/{horodatage}/database.sqlite`, relancer l'application.
   Aucun fichier image n'a besoin d'être restauré séparément : aucun original n'est jamais modifié
   ni déplacé par ce processus.

## 14. Procédure future de purge définitive (hors périmètre de cette intervention)

Une fois qu'un délai de sécurité suffisant se sera écoulé et qu'une revue humaine des exemplaires en
corbeille aura eu lieu, la suppression physique définitive existe déjà (`/admin/media/trash`,
action réservée aux super-administrateurs, exige de taper « SUPPRIMER », bloque si des relations non
détachées subsistent). **Aucune purge définitive n'a été effectuée ni programmée par cette
intervention** — la corbeille reste le seul état final des exemplaires fusionnés.

## 15. Premier lot de 20 groupes exacts

Exécuté le 24 juillet 2026, sur validation explicite, après la réussite complète du test à un seul
groupe (§9). Commande : `php artisan media:deduplicate --execute --batch=20`, confirmation
interactive réelle saisie (pas de `--force`).

### Contrôles préalables (avant exécution)

- Dry-run relancé à neuf pour confirmer l'état courant : **77 groupes exacts** détectés (78 − 1, le
  groupe de la validation précédente n'a plus de doublon actif), **64 éligibles**, **13 ambigus**,
  **523 quasi-doublons visuels** — chiffres inchangés pour ces deux dernières catégories, comme
  attendu.
- **Liste des 20 groupes qui allaient être traités**, affichée avant toute action (extraite du
  rapport dry-run régénéré) :

| Groupe | SHA1 (8) | Canonique | Copies |
|---|---|---|---|
| 1 | 04e6342f | img-01514 | 5 |
| 2 | 05879d7b | img-01478 | 17 |
| 3 | 0cd8a151 | img-01820 | 1 |
| 4 | 1290aa7c | img-01741 | 1 |
| 5 | 1dfeed09 | img-01502 | 8 |
| 6 | 20b9f3e4 | img-01496 | 1 |
| 7 | 246a2b72 | img-01564 | 4 |
| 8 | 2658e7fd | img-01740 | 1 |
| 9 | 2c741715 | img-01872 | 1 |
| 10 | 2c74f372 | img-01485 | 2 |
| 11 | 2efd1e02 | img-01658 | 1 |
| 12 | 2f05ab30 | img-01808 | 1 |
| 13 | 32890e04 | img-01501 | 9 |
| 14 | 36fc7e2c | img-01807 | 1 |
| 16 | 3d8ceb3d | img-03792 | 1 |
| 17 | 3ef4a050 | img-01511 | 24 |
| 18 | 44954f86 | img-04109 | 2 |
| 19 | 56f5127d | img-01490 | 4 |
| 20 | 584920df | img-01486 | 1 |
| 21 | 5bab32a9 | img-01621 | 16 |

  (Numérotation non continue : les groupes 15 ambigu est sauté, la sélection prend les 20 premiers
  groupes **non ambigus** dans l'ordre du tri SHA1.)

- **Confirmé avant exécution** : ces 20 groupes proviennent exclusivement de
  `findExactGroups()` (SHA1 strictement identique). La méthode `findVisuallySimilarGroups()`
  (523 quasi-doublons) n'est appelée par aucun chemin de code du mode `--execute` — structurellement
  impossible qu'un quasi-doublon visuel soit inclus dans ce lot.
- **Confirmé avant exécution** : aucun de ces 20 groupes n'impliquait de transfert d'album, de
  couverture d'album, de couverture de chronologie ou de statut Hero (vérifié via le rapport
  dry-run) — premier lot à risque intrinsèquement faible.
- État de référence capturé avant exécution : 5 179 médias au total (corbeille incluse), 431 déjà en
  corbeille, 4 Hero actifs, 9 albums, 741 lignes de pivot `album_media`, 0 couverture de chronologie
  définie, 74 entrées de journal d'activité.

### Sauvegarde

`storage/app/backups/media-deduplication/2026-07-24-171608/` — copie de `database.sqlite`
(4,68 Mo), export `media-export.json`/`.csv` des médias concernés par ce lot précisément.

### Résultat de l'exécution

```
Termine : 20 groupe(s) fusionne(s), 0 echec(s), 13 ignores (ambigus).
```

- **20 groupes traités**, **101 exemplaires mis à la corbeille** (soft delete), **0 échec**.
- Les 13 groupes ambigus détectés par le dry-run ont été **automatiquement exclus**, sans
  intervention nécessaire — comportement conforme.
- Les médias canoniques retenus sont ceux déjà proposés par le dry-run (tableau ci-dessus) — aucune
  divergence entre la simulation et l'exécution réelle.

### Contrôle complet après exécution

| Vérification | Avant | Après | Résultat |
|---|---|---|---|
| Médias totaux (corbeille incluse) | 5 179 | 5 179 | **Inchangé — aucune ligne supprimée** |
| Médias en corbeille | 431 | 532 | **+101, exact** |
| Hero actifs | 4 | 4 | **Inchangé** (aucun de ces groupes n'en contenait) |
| Albums | 9 | 9 | **Inchangé** |
| Lignes de pivot `album_media` | 741 | 741 | **Inchangé** (aucun transfert dans ce lot) |
| Couvertures de chronologie définies | 0 | 0 | **Inchangé** |
| Entrées de journal d'activité | 74 | 176 | **+102** (101 `dedup-merge` + 1 `dedup-batch`) |

- **Aucune suppression physique** : les fichiers originaux des deux paires vérifiées visuellement
  (voir ci-dessous) sont toujours présents sur le disque.
- **Aucune référence orpheline** : 0 média Hero actif trouvé en corbeille ; les 9 couvertures
  d'album pointent toutes vers un média actif (vérifié individuellement, album par album — deux
  paires d'albums partagent la même image de couverture, ce qui explique un premier calcul agrégé
  trompeur à 7/9, corrigé par une vérification unitaire qui confirme bien 9/9 couvertures valides) ;
  aucune couverture de chronologie n'est actuellement définie, donc rien à invalider sur ce point.
- **Carrousel Hero fonctionnel** : nombre d'images actives inchangé (4), page d'accueil vérifiée en
  HTTP 200.
- **Couvertures d'albums valides** : 9/9, vérifié individuellement par requête directe sur chaque
  album.
- **Albums valides** : 9 albums, 741 lignes de pivot inchangées, aucune ligne dupliquée introduite.
- **Chronologie valide** : aucune couverture définie avant ou après ce lot — rien à transférer,
  aucune régression possible sur ce point pour ce lot précis.
- **Pages publiques** — toutes vérifiées en HTTP 200 après l'exécution :
  `/` (accueil), `/galerie`, `/parcours`, `/atl`, `/mouvement`, `/albums`, `/videos`.
- **Contrôle visuel** de deux paires canonique/copie parmi les plus importantes de ce lot :
  - Groupe 17 — `img-01511` (canonique) / `img-01651` (copie, l'une des 24) : bannière
    « Ville de Kinshasa — Commune de Kintambo », photographies strictement identiques confirmées à
    l'œil.
  - Groupe 2 — `img-01478` (canonique) / `img-01622` (copie, l'une des 17) : logo feuille verte,
    photographies strictement identiques confirmées à l'œil.
- **Corbeille** : les 101 nouvelles entrées portent toutes la raison
  « Doublon exact fusionne vers {canonique} (sauvegarde 2026-07-24-171608) », horodatées
  précisément à l'exécution du lot — distinctes des 431 entrées préexistantes (dont une partagée
  avec le groupe 17, déjà en corbeille pour une raison sans rapport, « Suppression groupee »,
  antérieure à cette intervention — signalé pour transparence, non lié au dédoublonnage).
- **Journal d'activité** : 101 entrées `dedup-merge` correspondant exactement à 20 médias canoniques
  distincts (vérifié par comptage groupé), plus 1 entrée `dedup-batch` récapitulative pour cette
  exécution.

### Problèmes ou groupes ignorés

**Aucune anomalie détectée.** Les 13 groupes ambigus déjà identifiés par le dry-run (conflits de
métadonnées sur `title`/`event`) ont été ignorés comme prévu, sans nécessiter d'intervention. Aucun
échec de fusion, aucune transaction annulée, aucune incohérence constatée dans les contrôles
ci-dessus.

### Tests

Suite complète relancée après l'exécution du lot : **174 tests, 425 assertions, tous passants**.
Intégrité du dossier `images/` reconfirmée : 5 335 fichiers, nombre strictement inchangé depuis le
début de cette intervention.

### Arrêt conforme à la consigne

Conformément à la demande explicite, **aucun deuxième lot n'a été lancé**. 44 groupes éligibles
restent non traités (64 éligibles − 20 traités), ainsi que les 13 groupes ambigus et les 523
quasi-doublons visuels, qui ne seront de toute façon jamais traités automatiquement.
