Add scan flow MVP and local Axiom skill workspace

This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
Matthias
2026-04-19 21:11:32 +02:00
parent 577214d474
commit a60a76b797
679 changed files with 138964 additions and 73 deletions

View File

@@ -0,0 +1,132 @@
import Combine
import Foundation
import SwiftUI
enum ScanRoute: Hashable {
case result
case manual
}
@MainActor
final class ScanFlowModel: ObservableObject {
@Published var path: [ScanRoute] = []
@Published var currentSession: RecognitionSession?
@Published var isRecognizing = false
@Published var transientMessage: String?
let cameraService: CameraService
let photoLibraryService: PhotoLibraryService
let networkMonitor: NetworkMonitor
private let pipeline: CardRecognitionPipeline
init() {
let cameraService = CameraService()
let photoLibraryService = PhotoLibraryService()
let networkMonitor = NetworkMonitor()
self.cameraService = cameraService
self.photoLibraryService = photoLibraryService
self.networkMonitor = networkMonitor
self.pipeline = CardRecognitionPipeline(
networkStatusProvider: networkMonitor,
cloudOCRClient: StubCloudOCRClient(),
fallbackOCR: VisionCardOCRService(),
enhancer: NoOpCardFieldEnhancer()
)
}
func startObservers() {
networkMonitor.startMonitoring()
}
func stopObservers() {
networkMonitor.stopMonitoring()
cameraService.stopSession()
}
func prepareVisibleServices() async {
await cameraService.prepareIfAuthorized()
await photoLibraryService.refreshRecentsIfPossible()
}
func enableCamera() async {
await cameraService.requestAccessAndStart()
}
func capturePhoto() async {
do {
isRecognizing = true
let image = try await cameraService.capturePhoto()
try await recognize(image: image)
} catch {
transientMessage = error.localizedDescription
}
isRecognizing = false
}
func importRecentPhoto(_ item: RecentPhotoItem) async {
isRecognizing = true
defer { isRecognizing = false }
guard let image = await photoLibraryService.loadImage(for: item) else {
transientMessage = "The selected photo could not be loaded."
return
}
do {
try await recognize(image: image)
} catch {
currentSession = RecognitionSession(
draft: CardRecognitionDraft.manualPrefill(rawText: currentSession?.draft.rawText ?? ""),
thumbnailJPEGData: currentSession?.thumbnailJPEGData
)
path = [.manual]
transientMessage = error.localizedDescription
}
}
func startManualEntry() {
if currentSession == nil {
currentSession = RecognitionSession(draft: .manualPrefill())
} else {
currentSession?.draft.source = .manual
currentSession?.draft.confidence = .low
}
path = [.manual]
}
func openManualEntryFromResult() {
guard currentSession != nil else {
startManualEntry()
return
}
currentSession?.draft.source = .manual
if path.last != .manual {
path.append(.manual)
}
}
func updateDraft(_ draft: CardRecognitionDraft) {
currentSession?.draft = draft
}
func returnToScan(message: String? = nil) {
currentSession = nil
path = []
transientMessage = message
}
private func recognize(image: UIImage) async throws {
let session = try await pipeline.recognizeCard(in: image)
currentSession = session
if session.draft.hasDetectedContent {
path = [.result]
} else {
currentSession?.draft.source = .manual
currentSession?.draft.confidence = .low
path = [.manual]
}
}
}