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:
138
StackDex/Models/CardRecognitionDraft.swift
Normal file
138
StackDex/Models/CardRecognitionDraft.swift
Normal file
@@ -0,0 +1,138 @@
|
||||
import Foundation
|
||||
|
||||
enum RecognitionSource: String, Codable, CaseIterable, Identifiable {
|
||||
case cloud
|
||||
case onDeviceOffline
|
||||
case onDeviceFallback
|
||||
case manual
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .cloud:
|
||||
return "Cloud OCR"
|
||||
case .onDeviceOffline:
|
||||
return "On-Device (Offline)"
|
||||
case .onDeviceFallback:
|
||||
return "On-Device Fallback"
|
||||
case .manual:
|
||||
return "Manual Entry"
|
||||
}
|
||||
}
|
||||
|
||||
var detail: String {
|
||||
switch self {
|
||||
case .cloud:
|
||||
return "Prepared client boundary for future Convex/Mistral OCR."
|
||||
case .onDeviceOffline:
|
||||
return "Offline erkannt — Ergebnis kann weniger genau sein."
|
||||
case .onDeviceFallback:
|
||||
return "Cloud path unavailable — used the on-device Vision fallback."
|
||||
case .manual:
|
||||
return "No image is persisted after confirmation."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ConfidenceLevel: String, Codable, CaseIterable, Comparable {
|
||||
case low
|
||||
case medium
|
||||
case high
|
||||
|
||||
private var score: Int {
|
||||
switch self {
|
||||
case .low: return 0
|
||||
case .medium: return 1
|
||||
case .high: return 2
|
||||
}
|
||||
}
|
||||
|
||||
static func < (lhs: ConfidenceLevel, rhs: ConfidenceLevel) -> Bool {
|
||||
lhs.score < rhs.score
|
||||
}
|
||||
|
||||
var title: String { rawValue.capitalized }
|
||||
|
||||
var helperText: String {
|
||||
switch self {
|
||||
case .high:
|
||||
return "Looks solid — a quick confirmation should be enough."
|
||||
case .medium:
|
||||
return "Please review the extracted fields before confirming."
|
||||
case .low:
|
||||
return "Low confidence — manual corrections are recommended."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CardRarity: String, CaseIterable, Identifiable {
|
||||
case unknown = "Unknown"
|
||||
case common = "Common"
|
||||
case uncommon = "Uncommon"
|
||||
case rare = "Rare"
|
||||
case holoRare = "Holo Rare"
|
||||
case ultraRare = "Ultra Rare"
|
||||
case illustrationRare = "Illustration Rare"
|
||||
case specialArtRare = "Special Art Rare"
|
||||
case hyperRare = "Hyper Rare"
|
||||
case secretRare = "Secret Rare"
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
||||
struct CardRecognitionDraft: Equatable {
|
||||
var cardName: String
|
||||
var cardNumber: String
|
||||
var setIdentifier: String
|
||||
var rarity: String
|
||||
var source: RecognitionSource
|
||||
var confidence: ConfidenceLevel
|
||||
var notes: [String]
|
||||
var rawText: String
|
||||
|
||||
var hasDetectedContent: Bool {
|
||||
!cardName.isBlank || !cardNumber.isBlank || !setIdentifier.isBlank || !rarity.isBlank
|
||||
}
|
||||
|
||||
var combinedNumberAndSet: String {
|
||||
let components = [cardNumber.trimmedNilIfEmpty, setIdentifier.trimmedNilIfEmpty].compactMap { $0 }
|
||||
return components.joined(separator: " · ")
|
||||
}
|
||||
|
||||
static func manualPrefill(rawText: String = "") -> CardRecognitionDraft {
|
||||
CardRecognitionDraft(
|
||||
cardName: "",
|
||||
cardNumber: "",
|
||||
setIdentifier: "",
|
||||
rarity: CardRarity.unknown.rawValue,
|
||||
source: .manual,
|
||||
confidence: .low,
|
||||
notes: ["Manual entry is always available when OCR misses fields."],
|
||||
rawText: rawText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct RecognitionSession: Identifiable, Equatable {
|
||||
let id: UUID
|
||||
var draft: CardRecognitionDraft
|
||||
var thumbnailJPEGData: Data?
|
||||
|
||||
init(id: UUID = UUID(), draft: CardRecognitionDraft, thumbnailJPEGData: Data? = nil) {
|
||||
self.id = id
|
||||
self.draft = draft
|
||||
self.thumbnailJPEGData = thumbnailJPEGData
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var isBlank: Bool {
|
||||
trimmedNilIfEmpty == nil
|
||||
}
|
||||
|
||||
var trimmedNilIfEmpty: String? {
|
||||
let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user