spb/focale Public
Swift 100%
1//2// IndexStore.swift3// Focale4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//8// SwiftData store for the photo index. Photos are referenced by9// localIdentifier only — never duplicated, moved, or modified.10// Target: < 500 MB for 50 000 photos; thumbnails come from PhotoKit,11// never stored twice (CLAUDE.md §9).12//1314import Foundation15import SwiftData1617@Model18final class PhotoRecord {19 @Attribute(.unique) var localIdentifier: String20 var captureDate: Date?2122 // Stage progress23 var visionIndexedAt: Date?24 var semanticIndexedAt: Date?25 /// Model guardrails refused the image — a normal state, not an error26 /// (CLAUDE.md §8). Stage-1 data remains fully usable.27 var semanticRefused: Bool2829 // Stage 1 — Vision (real data)30 var ocrText: String?31 var featurePrint: Data?32 var classificationLabels: [String]33 var faceCount: Int3435 // Capture context (real data, free at shutter time)36 var contextJSON: Data?37 var projectName: String?38 var subjectHint: String?39 var placeName: String?40 var placeLocality: String?4142 // Stage 2 — Foundation Models (generated data, displayed as such)43 var gist: String?44 var kindRawValue: String?45 var entities: [String]46 var hasActionableInfo: Bool4748 var isFavorite: Bool4950 init(localIdentifier: String, captureDate: Date?) {51 self.localIdentifier = localIdentifier52 self.captureDate = captureDate53 self.semanticRefused = false54 self.classificationLabels = []55 self.faceCount = 056 self.entities = []57 self.hasActionableInfo = false58 self.isFavorite = false59 }6061 var captureContext: CaptureContext? {62 get {63 guard let contextJSON else { return nil }64 return try? JSONDecoder().decode(CaptureContext.self, from: contextJSON)65 }66 set {67 contextJSON = newValue.flatMap { try? JSONEncoder().encode($0) }68 projectName = newValue?.projectName69 subjectHint = newValue?.subjectHint70 placeName = newValue?.place?.name71 placeLocality = newValue?.place?.locality72 }73 }74}7576/// Incremental resume point — indexing never restarts from scratch.77@Model78final class IndexCheckpoint {79 /// Creation date of the oldest photo already backfilled (newest → oldest).80 var oldestIndexedDate: Date?81 var updatedAt: Date8283 init(oldestIndexedDate: Date? = nil) {84 self.oldestIndexedDate = oldestIndexedDate85 self.updatedAt = .now86 }87}8889enum IndexStore {90 static let container: ModelContainer = {91 let schema = Schema([PhotoRecord.self, IndexCheckpoint.self])92 let configuration = ModelConfiguration(schema: schema)93 do {94 return try ModelContainer(for: schema, configurations: [configuration])95 } catch {96 fatalError("Failed to create index store: \(error)")97 }98 }()99}100