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

@@ -7,13 +7,113 @@
import Testing
@testable import StackDex
import UIKit
struct StackDexTests {
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
// Swift Testing Documentation
// https://developer.apple.com/documentation/testing
@Test func cameraSessionStateBlocksStartUntilConfigurationCommits() {
var state = CameraSessionState()
#expect(state.canStartSession)
state.beginConfiguration()
#expect(!state.canStartSession)
state.commitConfiguration()
#expect(state.canStartSession)
}
@Test func cameraSessionStateTracksNestedConfigurationDepth() {
var state = CameraSessionState()
state.beginConfiguration()
state.beginConfiguration()
#expect(!state.canStartSession)
state.commitConfiguration()
#expect(!state.canStartSession)
state.commitConfiguration()
#expect(state.canStartSession)
}
@Test func heuristicExtractorFindsStructuredFields() {
let extractor = CardTextHeuristicExtractor()
let payload = OCRTextPayload(
rawText: "Charizard\n120 HP\n4/102 Base Set\nHolo Rare",
lines: [
RecognizedTextLine(text: "Charizard", confidence: 0.95, normalizedBounds: CGRect(x: 0.1, y: 0.82, width: 0.4, height: 0.08)),
RecognizedTextLine(text: "120 HP", confidence: 0.81, normalizedBounds: CGRect(x: 0.75, y: 0.83, width: 0.12, height: 0.05)),
RecognizedTextLine(text: "4/102 Base Set", confidence: 0.92, normalizedBounds: CGRect(x: 0.2, y: 0.12, width: 0.4, height: 0.05)),
RecognizedTextLine(text: "Holo Rare", confidence: 0.88, normalizedBounds: CGRect(x: 0.7, y: 0.1, width: 0.2, height: 0.05)),
],
averageConfidence: 0.89
)
let draft = extractor.extract(payload: payload, source: .onDeviceOffline)
#expect(draft.cardName == "Charizard")
#expect(draft.cardNumber == "4/102")
#expect(draft.setIdentifier == "Base Set")
#expect(draft.rarity == CardRarity.holoRare.rawValue)
#expect(draft.confidence == .high)
}
@Test func pipelineFallsBackToVisionWhenCloudIsStubbed() async throws {
let monitor = TestNetworkStatusProvider(isOnline: true)
let pipeline = CardRecognitionPipeline(
networkStatusProvider: monitor,
cloudOCRClient: StubCloudOCRClient(),
fallbackOCR: TestOCRService(payload: OCRTextPayload(
rawText: "Pikachu\n25/165\nScarlet & Violet\nRare",
lines: [
RecognizedTextLine(text: "Pikachu", confidence: 0.95, normalizedBounds: CGRect(x: 0.1, y: 0.8, width: 0.3, height: 0.08)),
RecognizedTextLine(text: "25/165", confidence: 0.91, normalizedBounds: CGRect(x: 0.3, y: 0.15, width: 0.18, height: 0.05)),
RecognizedTextLine(text: "Scarlet & Violet", confidence: 0.87, normalizedBounds: CGRect(x: 0.25, y: 0.11, width: 0.4, height: 0.05)),
RecognizedTextLine(text: "Rare", confidence: 0.75, normalizedBounds: CGRect(x: 0.76, y: 0.1, width: 0.12, height: 0.04)),
],
averageConfidence: 0.87
))
)
let session = try await pipeline.recognizeCard(in: TestImageFactory.makeImage())
#expect(session.draft.source == .onDeviceFallback)
#expect(session.draft.cardName == "Pikachu")
#expect(session.draft.cardNumber == "25/165")
#expect(session.draft.notes.contains(where: { $0.contains("Cloud OCR") }))
}
}
private final class TestNetworkStatusProvider: NetworkStatusProviding {
var isOnline: Bool
init(isOnline: Bool) {
self.isOnline = isOnline
}
func startMonitoring() {}
func stopMonitoring() {}
}
private struct TestOCRService: CardTextRecognizing {
let payload: OCRTextPayload
func recognizeText(in image: PreparedImage) async throws -> OCRTextPayload {
payload
}
}
private enum TestImageFactory {
static func makeImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 300, height: 420))
return renderer.image { context in
UIColor.white.setFill()
context.fill(CGRect(x: 0, y: 0, width: 300, height: 420))
UIColor.black.setStroke()
context.cgContext.setLineWidth(6)
context.cgContext.stroke(CGRect(x: 16, y: 16, width: 268, height: 388))
}
}
}