feat(scanner): add native artifact pipeline
Add native IK-style capture processing, Artifact Inventory, explicit promotion and single-result review. Confirm the three live OCR corrections in the eval corpus and preserve extraction/value separation.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { hashId } from "./artifactStore.js";
|
||||
import type { ParsedArtifactCandidate } from "./artifactOcrParser.js";
|
||||
import type {
|
||||
ArtifactValueStatus,
|
||||
ScanResultFieldConfidence,
|
||||
ScanResultIkMatch,
|
||||
ScanResultExtractionStatus,
|
||||
ScanResultCategory,
|
||||
StoredScanResultEntry,
|
||||
} from "../types/storage.js";
|
||||
|
||||
export interface ScanResultEntryInput {
|
||||
runId: string;
|
||||
sequence: number;
|
||||
page?: number;
|
||||
row?: number;
|
||||
col?: number;
|
||||
category?: ScanResultCategory;
|
||||
source: string;
|
||||
imagePath: string;
|
||||
parsed: ParsedArtifactCandidate | null;
|
||||
needsReview: boolean;
|
||||
confidence: number;
|
||||
capturedAt?: string;
|
||||
artifactRecordId?: string;
|
||||
persistedArtifact?: boolean;
|
||||
notes?: string[];
|
||||
error?: string;
|
||||
locked?: boolean;
|
||||
ikMatch?: ScanResultIkMatch;
|
||||
}
|
||||
|
||||
export function deriveScanResultExtractionStatus(input: {
|
||||
parsed: boolean;
|
||||
needsReview: boolean;
|
||||
error?: string;
|
||||
}): ScanResultExtractionStatus {
|
||||
if (input.error === "Card crop image missing.") return "missing_crop";
|
||||
if (input.error) return input.parsed ? "review" : "error";
|
||||
if (!input.parsed) return "parse_error";
|
||||
if (input.needsReview) return "review";
|
||||
return "parsed";
|
||||
}
|
||||
|
||||
export function deriveArtifactValueStatus(extractionStatus: ScanResultExtractionStatus): ArtifactValueStatus {
|
||||
return extractionStatus === "parsed" ? "deferred" : "review";
|
||||
}
|
||||
|
||||
export function createStoredScanResultEntry(input: ScanResultEntryInput): StoredScanResultEntry {
|
||||
const category = input.category ?? "artifact";
|
||||
const extractionStatus = deriveScanResultExtractionStatus({
|
||||
parsed: Boolean(input.parsed),
|
||||
needsReview: input.needsReview,
|
||||
error: input.error,
|
||||
});
|
||||
const id = hashId([
|
||||
input.runId,
|
||||
input.sequence,
|
||||
category,
|
||||
input.imagePath,
|
||||
input.parsed?.name ?? "",
|
||||
input.parsed?.slot ?? "",
|
||||
input.error ?? "",
|
||||
].join("::"));
|
||||
|
||||
return {
|
||||
id,
|
||||
runId: input.runId,
|
||||
sequence: input.sequence,
|
||||
page: input.page,
|
||||
row: input.row,
|
||||
col: input.col,
|
||||
category,
|
||||
source: input.source,
|
||||
imagePath: input.imagePath,
|
||||
capturedAt: input.capturedAt ?? new Date(0).toISOString(),
|
||||
extractionStatus,
|
||||
extractionConfidence: Math.max(0, Math.round(input.confidence || 0)),
|
||||
needsReview: extractionStatus !== "parsed",
|
||||
valueStatus: deriveArtifactValueStatus(extractionStatus),
|
||||
valueScore: null,
|
||||
artifact: input.parsed
|
||||
? {
|
||||
name: input.parsed.name,
|
||||
slot: input.parsed.slot,
|
||||
level: input.parsed.level,
|
||||
setName: input.parsed.setName,
|
||||
mainStat: input.parsed.mainStat,
|
||||
mainValue: input.parsed.mainValue,
|
||||
substats: [...input.parsed.substats],
|
||||
equipped: input.parsed.equipped,
|
||||
locked: input.locked,
|
||||
}
|
||||
: undefined,
|
||||
ikMatch: input.ikMatch,
|
||||
fieldConfidences: input.parsed ? parsedFieldConfidences(input.parsed) : undefined,
|
||||
artifactRecordId: input.artifactRecordId,
|
||||
persistedArtifact: Boolean(input.persistedArtifact),
|
||||
notes: [...(input.notes ?? input.parsed?.notes ?? [])],
|
||||
error: input.error,
|
||||
};
|
||||
}
|
||||
|
||||
function parsedFieldConfidences(parsed: ParsedArtifactCandidate): ScanResultFieldConfidence[] {
|
||||
return [
|
||||
fieldConfidence("name", "Name", parsed.fields.name),
|
||||
fieldConfidence("slot", "Slot", parsed.fields.slot),
|
||||
fieldConfidence("level", "Level", parsed.fields.level),
|
||||
fieldConfidence("mainStat", "Main stat", parsed.fields.mainStat),
|
||||
fieldConfidence("mainValue", "Main value", parsed.fields.mainValue),
|
||||
fieldConfidence("setName", "Set", parsed.fields.setName),
|
||||
fieldConfidence("equipped", "Equipped", parsed.fields.equipped),
|
||||
fieldConfidence("substats", "Substats", parsed.fields.substats),
|
||||
].filter((entry): entry is ScanResultFieldConfidence => Boolean(entry));
|
||||
}
|
||||
|
||||
function fieldConfidence(
|
||||
key: string,
|
||||
label: string,
|
||||
parsedField: ParsedArtifactCandidate["fields"][keyof ParsedArtifactCandidate["fields"]] | undefined,
|
||||
): ScanResultFieldConfidence | null {
|
||||
if (!parsedField) return null;
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
value: parsedField.value,
|
||||
confidence: Math.max(0, Math.round(parsedField.confidence || 0)),
|
||||
source: parsedField.source,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user