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,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
}
}

View File

@@ -0,0 +1,46 @@
import Foundation
import SwiftData
@Model
final class ConfirmedScanRecord {
var confirmedAt: Date
var cardName: String
var cardNumber: String
var setIdentifier: String
var rarity: String
var recognitionSource: String
var confidence: String
var rawTextPreview: String
init(
confirmedAt: Date = .now,
cardName: String,
cardNumber: String,
setIdentifier: String,
rarity: String,
recognitionSource: String,
confidence: String,
rawTextPreview: String
) {
self.confirmedAt = confirmedAt
self.cardName = cardName
self.cardNumber = cardNumber
self.setIdentifier = setIdentifier
self.rarity = rarity
self.recognitionSource = recognitionSource
self.confidence = confidence
self.rawTextPreview = rawTextPreview
}
convenience init(draft: CardRecognitionDraft) {
self.init(
cardName: draft.cardName,
cardNumber: draft.cardNumber,
setIdentifier: draft.setIdentifier,
rarity: draft.rarity,
recognitionSource: draft.source.rawValue,
confidence: draft.confidence.rawValue,
rawTextPreview: String(draft.rawText.prefix(240))
)
}
}