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.
139 lines
3.7 KiB
Swift
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
|
|
}
|
|
}
|