639b0b7f59
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.
131 lines
4.1 KiB
TypeScript
131 lines
4.1 KiB
TypeScript
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,
|
|
};
|
|
}
|