# Focale
**Appareil photo manuel + photothèque interrogeable.** SwiftUI, AVFoundation, PhotoKit, Vision, Foundation Models. Deux produits qui se sauvent mutuellement : une photo prise dans Focale arrive déjà comprise, et la vieille bibliothèque se rattrape en arrière-plan pendant que le produit est déjà utile.
**Rien ne quitte l'appareil. Jamais.** Aucune photo dupliquée, déplacée ou modifiée. Utile avant que l'index soit complet.
**Author :** Simon-Pierre Boucher — contact@spboucher.ai
> La thèse produit, les contraintes API et les règles d'architecture vivent dans [CLAUDE.md](CLAUDE.md) — le lire avant toute modification.
---
## Captures d'écran
| Appareil | Bibliothèque | Visionneuse |
|:---:|:---:|:---:|
|  |  |  |
| Recettes, projets, retardateur, grille, dials manuels | Albums vivants + progression d'indexation honnête | Zoom, favoris, « photos semblables », panneau d'info |
*(Simulateur iPhone 17 Pro — l'aperçu caméra est noir sans matériel photo.)*
---
## Métriques
| Métrique | Valeur |
|---|---|
| Fichiers Swift | 37 |
| Lignes de code Swift | 4 554 |
| Erreurs de build | 0 |
| Concurrence | Swift 6, `SWIFT_STRICT_CONCURRENCY: complete` |
| Cible de déploiement | iOS 26.0 (iPhone) |
| Tests UI | 1 suite (régression crash favoris) — ✅ 22,7 s |
| Builds TestFlight | 4 (0.1.0 (1) → (4)) |
| Étapes CLAUDE.md couvertes | 1–2 complètes, fondations 3–5 |
| Module | LOC | Rôle |
|---|---|---|
| `Capture/` | 1 999 | session AVFoundation, contrôles manuels, ProRAW, flash, retardateur, recettes, dispositions, gestes |
| `Index/` | 676 | étage 1 Vision (tout) + étage 2 Foundation Models (candidats seulement) |
| `Library/` | 661 | timeline, visionneuse plein écran, albums vivants |
| `Search/` | 508 | requête naturelle → filtre structuré → classement local |
| `Intent/` | 341 | `CaptureContext` — le contexte capturé au déclenchement, cœur du projet |
| `App/` | 246 | entrée, onboarding accès photos (le risque produit n° 1) |
| `FocaleUITests/` | 74 | test de régression du flux favoris |
| `Design/` | 49 | tokens, badge « Générée » |
---
## Fonctionnalités
### 📷 Appareil photo (étape 1 — vendable seul)
- **Contrôles manuels complets** : ISO, vitesse d'obturation, mise au point, balance des blancs, compensation d'exposition, zoom — chaque contrôle est **vérifié contre le matériel** avant d'être affiché (aucun bouton mort).
- **ProRAW** (iPhone 12 Pro+), HEIC, choix d'objectif (ultra grand-angle / grand-angle / télé).
- **Flash** auto / activé / désactivé (affiché seulement si l'appareil a un flash).
- **Retardateur** 3 s / 10 s avec compte à rebours annulable, **grille des tiers**, **retour au déclenchement** (clignotement d'écran + haptique immédiate).
- **Le déclencheur est sacré** : aucune inférence, aucun disque, aucune allocation dans le chemin du déclenchement — réponse < 50 ms visée.
- **Recettes** : presets complets nommés, rappelables en un geste, **exportables en fichier `.focalerecipe`** et importables — elles se partagent.
- **Dispositions** : trois profils (Simple / Photographe / Expert), gestes entièrement réassignables (glissement gauche = ISO, pincement = zoom…), haptique paramétrable par contrôle.
- Proposition de recette sur signal de scène (basse lumière) — **une proposition, jamais un basculement silencieux**.
### 🎯 Contexte de capture (étape 2 — la pièce qui différencie tout)
- Au déclenchement, Focale enregistre **sans coût d'inférence** : recette active, projet déclaré, réglages manuels, signal de scène (luminosité EV), lieu grossier (jamais de coordonnées brutes).
- **Écrit dans les métadonnées EXIF de la photo** (sans ré-encodage) en plus de la base locale : si l'utilisateur désinstalle, l'information reste dans sa photo.
- **Mode Projet** : déclare « chantier cuisine » une fois — tout ce qui suit est marqué. Zéro friction.
- **Note de sujet après la prise** (« reçu du garage ») — jamais de saisie obligatoire avant une photo.
### 🔍 Indexation & recherche (étapes 3–5)
- **Étage 1 — Vision, sur tout** : OCR (le gagnant silencieux : reçus, tableaux blancs, numéros de série), empreintes visuelles (similarité sans LLM), classification, visages (comptage local seulement, jamais d'identification).
- **Étage 2 — Foundation Models, sur candidats seulement** : le calcul à ne jamais oublier — 40 000 photos × 1-3 s = 11-33 h en série. L'étage 2 ne touche que les photos qui le méritent (prises dans Focale, favorites, demandées).
- **Du plus récent au plus ancien, toujours.** Photos récentes indexées au premier plan dès l'ouverture ; rattrapage profond la nuit, en charge (`BGProcessingTask`), reprise incrémentale.
- **Recherche naturelle** : « le reçu du garage l'automne passé » → filtre structuré exécuté sur la base locale (instantané). Le modèle analyse la requête, **jamais les photos**. Repli déterministe complet sur appareils sans A17 Pro.
- **Albums vivants** : définis par une requête, ils se remplissent tout seuls. Créables depuis n'importe quelle recherche.
- **« Photos semblables »** : distance d'empreinte visuelle, zéro LLM.
- **Honnêteté** : contenu généré toujours badgé « Générée », l'OCR prime sur le modèle pour tout chiffre, progression d'indexation transparente.
---
## Architecture
```
Focale/
├─ App/ entrée, onboarding accès photos, racine
├─ Capture/
│ ├─ Session/ AVCaptureSession (file série dédiée), capacités matérielles
│ ├─ Controls/ modèle ISO/vitesse/focus/BB, dials, mapping de gestes
│ ├─ Output/ délégué de capture, écriture PhotoKit + EXIF
│ ├─ Recipes/ presets, gestionnaire, export/import
│ └─ Layout/ profils d'interface Simple/Photographe/Expert
├─ Intent/ CaptureContext, projets, signal de scène, lieu
├─ Index/
│ ├─ Vision/ étage 1 — OCR, feature prints, classification
│ ├─ Semantic/ étage 2 — Foundation Models (@Generable)
│ ├─ Store/ SwiftData (localIdentifier, jamais de doublon)
│ └─ Scheduler/ pipeline + BGProcessingTask (en charge seulement)
├─ Search/ analyse de requête (FM ou déterministe) + classement
├─ Library/ timeline, visionneuse, albums vivants
└─ Design/ tokens, badge « Générée »
FocaleUITests/ tests de régression UI
Design/AppIcon.svg icône source (rendue en PNG dans Assets.xcassets)
```
Le projet Xcode est **généré par XcodeGen** : `Focale.xcodeproj` est un artefact, la source de vérité est `project.yml`.
---
## Build & tests
```bash
xcodegen generate # regénère Focale.xcodeproj depuis project.yml
open Focale.xcodeproj # build & run (la caméra exige un iPhone physique)
# Tests UI (simulateur)
xcodebuild test -project Focale.xcodeproj -scheme Focale \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'
```
- Xcode 26+, Swift 6 (concurrence stricte), cible iOS 26.0.
- Foundation Models exige un appareil A17 Pro+ ; partout ailleurs le chemin Vision-only prend le relais automatiquement.
### TestFlight
```bash
xcodebuild -project Focale.xcodeproj -scheme Focale -destination 'generic/platform=iOS' \
-archivePath build/Focale-N.xcarchive archive -allowProvisioningUpdates
open -a Xcode build/Focale-N.xcarchive # Organizer → Distribute App
```
| Build | Contenu |
|---|---|
| 0.1.0 (1) | fondation complète : caméra manuelle, contexte, index, recherche |
| 0.1.0 (2) | permission caméra, visionneuse, orientation portrait, indexation au premier plan |
| 0.1.0 (3) | projets/recettes dans l'UI, export `.focalerecipe`, note après prise, pincement, favoris, « photos semblables », albums vivants depuis recherche |
| 0.1.0 (4) | **crashs d'isolation Swift 6 corrigés** (favoris, enregistrement de capture, vignettes), Vision résilient, flash, retardateur, grille, retour au déclenchement, test UI de régression |
---
## Leçons Swift 6 (à lire avant de toucher à PhotoKit)
Les closures passées à `PHPhotoLibrary.performChanges` ou aux handlers de `PHImageManager` **héritent de l'isolation** (MainActor ou acteur) du contexte où elles sont créées. PhotoKit les exécute sur sa propre file → `dispatch_assert_queue_fail`, crash immédiat. Règle du projet : **tout bloc destiné à une file de framework est déclaré `@Sendable`**, récupère ses `PHAsset` à l'intérieur, et sort ses résultats via une boîte `@unchecked Sendable`. Voir `PhotoLibraryWriter.swift` pour le patron.
---
## Icône
Source : `Design/AppIcon.svg` — iris d'obturateur à six lamelles ambre sur objectif charbon. Pour régénérer le PNG :
```bash
mkdir -p /tmp/focale-icon
qlmanage -t -s 1024 -o /tmp/focale-icon Design/AppIcon.svg
cp /tmp/focale-icon/AppIcon.svg.png Focale/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png
```
⚠️ Le dégradé des lamelles doit rester `gradientUnits="userSpaceOnUse"` : un dégradé `objectBoundingBox` sur une `` (boîte de surface nulle) ne se rend pas, par spécification SVG.
---
## Règles non négociables
1. **Rien ne quitte l'appareil. Jamais.** Aucune escalade cloud, même optionnelle, même sur les métadonnées.
2. **Aucune photo n'est dupliquée, déplacée ou modifiée.** Index par `localIdentifier` ; désinstaller ne fait rien perdre.
3. **Utile avant que l'index soit complet.** Résultats progressifs, du plus récent au plus ancien.