SPB Git

spb/focale Public

Swift 100%
3.1 KB · 84 lines swift
Raw Blame History
1//2//  SemanticIndexer.swift3//  Focale4//5//  Author: Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//8//  Stage 2 — Foundation Models, on candidates only (CLAUDE.md §5).9//  The math that rules everything: 40 000 photos × 1–3 s = 11–33 h, serial.10//  So this actor runs one inference at a time, only on photos that earn it.11//12//  Note: direct image input in the prompt arrives with the iOS 27 SDK;13//  until then stage 2 reasons over stage-1 signals + capture context,14//  which is already rich for Focale-taken photos.15//1617import Foundation18import FoundationModels1920/// Why a photo earned a stage-2 pass. Anything else stays Vision-only.21enum SemanticCandidateReason: Sendable {22    case capturedInFocale     // context already present: minimal cost, maximal value23    case userRequested        // the user opened the photo and asked24    case searchDeepening      // stage-1 search found nothing; deepen a small subset25    case markedImportant      // favorite, shared, frequently opened26}2728/// Inputs handed to the model — stage-1 signals, never the photo library.29struct SemanticCandidate: Sendable {30    var localIdentifier: String31    var ocrExcerpt: String?32    var classificationLabels: [String]33    var contextJSON: String?34}3536actor SemanticIndexer {3738    static var isModelAvailable: Bool {39        SystemLanguageModel.default.availability == .available40    }4142    /// Serialized on the Neural Engine anyway (CLAUDE.md §2) — one session,43    /// one inference at a time.44    private var session: LanguageModelSession?4546    /// Returns nil on guardrail refusal — a normal state, no error surfaced47    /// (CLAUDE.md §8). The photo stays indexed by stage 1.48    func analyze(_ candidate: SemanticCandidate) async -> PhotoSemantics? {49        guard Self.isModelAvailable else { return nil }5051        let session = self.session ?? LanguageModelSession(instructions: """52            Tu analyses les signaux extraits d'une photo pour la rendre cherchable.53            Réponds uniquement à partir des signaux fournis. Sur incertitude, \54            reste vague plutôt que d'inventer. Ne devine jamais un chiffre, \55            un montant ou une date : ils viennent de l'OCR, pas de toi.56            """)57        self.session = session5859        var prompt = "Signaux de la photo :\n"60        if let ocr = candidate.ocrExcerpt, !ocr.isEmpty {61            prompt += "Texte OCR (données réelles) :\n\(String(ocr.prefix(1200)))\n"62        }63        if !candidate.classificationLabels.isEmpty {64            prompt += "Classification : \(candidate.classificationLabels.joined(separator: ", "))\n"65        }66        if let context = candidate.contextJSON {67            prompt += "Contexte de capture : \(context)\n"68        }6970        do {71            let response = try await session.respond(72                to: prompt,73                generating: PhotoSemantics.self74            )75            return response.content76        } catch {77            // Guardrail refusal or transient failure: treat as normal,78            // reset the session so one bad turn doesn't poison the next.79            self.session = nil80            return nil81        }82    }83}84