SPB Git

spb/focale Public

Swift 100%
2.9 KB · 100 lines swift
Raw Blame History
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