Files
genshin-assistant/src/lib/scanResultEntry.ts
T
AzuTear 639b0b7f59 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.
2026-07-09 23:30:42 +02:00

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