SPB Git

spb/focale Public

Swift 100%
10.5 KB · 178 lines markdown
Rendered Raw Blame History
1# Focale23<p align="center">4  <img src="Design/AppIcon.svg" width="160" alt="Icône Focale — iris d'obturateur à six lamelles"/>5</p>67<p align="center">8  <img src="https://img.shields.io/badge/iOS-26.0%2B-black?logo=apple" alt="iOS 26.0+"/>9  <img src="https://img.shields.io/badge/Swift-6-F05138?logo=swift&logoColor=white" alt="Swift 6"/>10  <img src="https://img.shields.io/badge/SwiftUI-%40Observable-0A84FF" alt="SwiftUI"/>11  <img src="https://img.shields.io/badge/concurrence-stricte-8A2BE2" alt="Strict concurrency"/>12  <img src="https://img.shields.io/badge/build-passing-brightgreen" alt="Build passing"/>13  <img src="https://img.shields.io/badge/tests%20UI-passing-brightgreen" alt="UI tests passing"/>14  <img src="https://img.shields.io/badge/TestFlight-0.1.0%20(4)-blue" alt="TestFlight 0.1.0 (4)"/>15  <img src="https://img.shields.io/badge/confidentialit%C3%A9-100%25%20sur%20l'appareil-FF9429" alt="100% on-device"/>16</p>1718**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.1920**Rien ne quitte l'appareil. Jamais.** Aucune photo dupliquée, déplacée ou modifiée. Utile avant que l'index soit complet.2122**Author :** Simon-Pierre Boucher — contact@spboucher.ai2324> La thèse produit, les contraintes API et les règles d'architecture vivent dans [CLAUDE.md](CLAUDE.md) — le lire avant toute modification.2526---2728## Captures d'écran2930| Appareil | Bibliothèque | Visionneuse |31|:---:|:---:|:---:|32| ![Écran de capture](docs/screenshots/capture.png) | ![Bibliothèque](docs/screenshots/bibliotheque.png) | ![Visionneuse](docs/screenshots/visionneuse.png) |33| Recettes, projets, retardateur, grille, dials manuels | Albums vivants + progression d'indexation honnête | Zoom, favoris, « photos semblables », panneau d'info |3435*(Simulateur iPhone 17 Pro — l'aperçu caméra est noir sans matériel photo.)*3637---3839## Métriques4041| Métrique | Valeur |42|---|---|43| Fichiers Swift | 37 |44| Lignes de code Swift | 4 554 |45| Erreurs de build | 0 |46| Concurrence | Swift 6, `SWIFT_STRICT_CONCURRENCY: complete` |47| Cible de déploiement | iOS 26.0 (iPhone) |48| Tests UI | 1 suite (régression crash favoris) — ✅ 22,7 s |49| Builds TestFlight | 4 (0.1.0 (1) → (4)) |50| Étapes CLAUDE.md couvertes | 1–2 complètes, fondations 3–5 |5152| Module | LOC | Rôle |53|---|---|---|54| `Capture/` | 1 999 | session AVFoundation, contrôles manuels, ProRAW, flash, retardateur, recettes, dispositions, gestes |55| `Index/` | 676 | étage 1 Vision (tout) + étage 2 Foundation Models (candidats seulement) |56| `Library/` | 661 | timeline, visionneuse plein écran, albums vivants |57| `Search/` | 508 | requête naturelle → filtre structuré → classement local |58| `Intent/` | 341 | `CaptureContext` — le contexte capturé au déclenchement, cœur du projet |59| `App/` | 246 | entrée, onboarding accès photos (le risque produit n° 1) |60| `FocaleUITests/` | 74 | test de régression du flux favoris |61| `Design/` | 49 | tokens, badge « Générée » |6263---6465## Fonctionnalités6667### 📷 Appareil photo (étape 1 — vendable seul)68- **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).69- **ProRAW** (iPhone 12 Pro+), HEIC, choix d'objectif (ultra grand-angle / grand-angle / télé).70- **Flash** auto / activé / désactivé (affiché seulement si l'appareil a un flash).71- **Retardateur** 3 s / 10 s avec compte à rebours annulable, **grille des tiers**, **retour au déclenchement** (clignotement d'écran + haptique immédiate).72- **Le déclencheur est sacré** : aucune inférence, aucun disque, aucune allocation dans le chemin du déclenchement — réponse < 50 ms visée.73- **Recettes** : presets complets nommés, rappelables en un geste, **exportables en fichier `.focalerecipe`** et importables — elles se partagent.74- **Dispositions** : trois profils (Simple / Photographe / Expert), gestes entièrement réassignables (glissement gauche = ISO, pincement = zoom…), haptique paramétrable par contrôle.75- Proposition de recette sur signal de scène (basse lumière) — **une proposition, jamais un basculement silencieux**.7677### 🎯 Contexte de capture (étape 2 — la pièce qui différencie tout)78- 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).79- **É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.80- **Mode Projet** : déclare « chantier cuisine » une fois — tout ce qui suit est marqué. Zéro friction.81- **Note de sujet après la prise** (« reçu du garage ») — jamais de saisie obligatoire avant une photo.8283### 🔍 Indexation & recherche (étapes 3–5)84- **É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).85- **É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).86- **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.87- **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.88- **Albums vivants** : définis par une requête, ils se remplissent tout seuls. Créables depuis n'importe quelle recherche.89- **« Photos semblables »** : distance d'empreinte visuelle, zéro LLM.90- **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.9192---9394## Architecture9596```97Focale/98├─ App/                entrée, onboarding accès photos, racine99├─ Capture/100│  ├─ Session/         AVCaptureSession (file série dédiée), capacités matérielles101│  ├─ Controls/        modèle ISO/vitesse/focus/BB, dials, mapping de gestes102│  ├─ Output/          délégué de capture, écriture PhotoKit + EXIF103│  ├─ Recipes/         presets, gestionnaire, export/import104│  └─ Layout/          profils d'interface Simple/Photographe/Expert105├─ Intent/             CaptureContext, projets, signal de scène, lieu106├─ Index/107│  ├─ Vision/          étage 1 — OCR, feature prints, classification108│  ├─ Semantic/        étage 2 — Foundation Models (@Generable)109│  ├─ Store/           SwiftData (localIdentifier, jamais de doublon)110│  └─ Scheduler/       pipeline + BGProcessingTask (en charge seulement)111├─ Search/             analyse de requête (FM ou déterministe) + classement112├─ Library/            timeline, visionneuse, albums vivants113└─ Design/             tokens, badge « Générée »114FocaleUITests/         tests de régression UI115Design/AppIcon.svg     icône source (rendue en PNG dans Assets.xcassets)116```117118Le projet Xcode est **généré par XcodeGen** : `Focale.xcodeproj` est un artefact, la source de vérité est `project.yml`.119120---121122## Build & tests123124```bash125xcodegen generate        # regénère Focale.xcodeproj depuis project.yml126open Focale.xcodeproj    # build & run (la caméra exige un iPhone physique)127128# Tests UI (simulateur)129xcodebuild test -project Focale.xcodeproj -scheme Focale \130  -destination 'platform=iOS Simulator,name=iPhone 17 Pro'131```132133- Xcode 26+, Swift 6 (concurrence stricte), cible iOS 26.0.134- Foundation Models exige un appareil A17 Pro+ ; partout ailleurs le chemin Vision-only prend le relais automatiquement.135136### TestFlight137138```bash139xcodebuild -project Focale.xcodeproj -scheme Focale -destination 'generic/platform=iOS' \140  -archivePath build/Focale-N.xcarchive archive -allowProvisioningUpdates141open -a Xcode build/Focale-N.xcarchive   # Organizer → Distribute App142```143144| Build | Contenu |145|---|---|146| 0.1.0 (1) | fondation complète : caméra manuelle, contexte, index, recherche |147| 0.1.0 (2) | permission caméra, visionneuse, orientation portrait, indexation au premier plan |148| 0.1.0 (3) | projets/recettes dans l'UI, export `.focalerecipe`, note après prise, pincement, favoris, « photos semblables », albums vivants depuis recherche |149| 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 |150151---152153## Leçons Swift 6 (à lire avant de toucher à PhotoKit)154155Les 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.156157---158159## Icône160161Source : `Design/AppIcon.svg` — iris d'obturateur à six lamelles ambre sur objectif charbon. Pour régénérer le PNG :162163```bash164mkdir -p /tmp/focale-icon165qlmanage -t -s 1024 -o /tmp/focale-icon Design/AppIcon.svg166cp /tmp/focale-icon/AppIcon.svg.png Focale/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png167```168169⚠️ Le dégradé des lamelles doit rester `gradientUnits="userSpaceOnUse"` : un dégradé `objectBoundingBox` sur une `<line>` (boîte de surface nulle) ne se rend pas, par spécification SVG.170171---172173## Règles non négociables1741751. **Rien ne quitte l'appareil. Jamais.** Aucune escalade cloud, même optionnelle, même sur les métadonnées.1762. **Aucune photo n'est dupliquée, déplacée ou modifiée.** Index par `localIdentifier` ; désinstaller ne fait rien perdre.1773. **Utile avant que l'index soit complet.** Résultats progressifs, du plus récent au plus ancien.178