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:
132
StackDex/Features/ScanFlowModel.swift
Normal file
132
StackDex/Features/ScanFlowModel.swift
Normal 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]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user