Files
stackdex_neu/StackDex/Models/CardRecognitionDraft.swift
Matthias a60a76b797 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.
2026-04-19 21:11:32 +02:00

139 lines
3.7 KiB
Swift

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