feat(worker/ai): Gemini-lärar-tier för scan/detect/allergen + shadow-capture

- Utöka GeminiAamosClient med ANALYZE_MEAL_IMAGE, READ_NUTRITION_LABEL,
  READ_EXPIRY_DATE och READ_RECEIPT (vision-prompts mot kontraktsscheman).
- Lägg till apps/worker/src/lib/shadow-capture.ts: S3/local fallback,
  imageTraining-gating, ingen PII, aldrig faila användarvägen.
- Integrera capture i scan-processorn efter lyckad runTask med consentFlags.
- Lägg till @aws-sdk/client-s3 i worker samt eval:capture-skript.
- Uppdatera eval:scan till Wikimedia Special:FilePath + redirect-following.
- Exkludera .terraform i brand-guard för att undvika false positives.
- Uppdatera gemini-test för DEDUPLICATE_INVENTORY som unsupported.
This commit is contained in:
Sven (AAMOS AI)
2026-08-12 02:51:05 +07:00
parent 515dd12cc7
commit 9a9f8a6a3c
9 changed files with 615 additions and 20 deletions
+30 -7
View File
@@ -6,6 +6,7 @@ import type { WorkerContext } from "../context.js";
import { getLocaleContext } from "../locale.js";
import type { LocaleContext } from "@app/shared-types";
import { recordAiUsage } from "../lib/ai-usage.js";
import { captureTrainingSample, type CaptureConsentFlags } from "../lib/shadow-capture.js";
/**
* Bild-/OCR-jobb (spec §54): hämtar scan_job, anropar AAMOS med kontraktvaliderad
@@ -28,7 +29,7 @@ export async function processScanJob(ctx: WorkerContext, scanJobId: string): Pro
const imageUrls = job.s3Keys.map((k) => ctx.readUrl(k));
const localeContext = await getLocaleContext(ctx, job.userId);
const consents = await loadConsentFlags(ctx, job.userId);
const consentFlags = await loadConsentFlags(ctx, job.userId);
const started = Date.now();
const result = await runAamosForJob(
@@ -38,6 +39,7 @@ export async function processScanJob(ctx: WorkerContext, scanJobId: string): Pro
imageUrls,
job.context,
localeContext,
consentFlags,
);
if (result.status === "failed" || result.output == null) {
@@ -80,6 +82,27 @@ export async function processScanJob(ctx: WorkerContext, scanJobId: string): Pro
await recordScanCompleted(ctx, job, result);
// Shadow-capture: spara träningspar för framtida AAMOS-distillation (FAS 1).
// Kör aldrig synkront på användarens kritiska väg; fel swallås.
if (result.status === "ok" && result.output != null) {
const capture = await captureTrainingSample({
taskType: job.jobType as AamosTaskType,
inputS3Keys: job.s3Keys,
output: result.output as Record<string, unknown>,
modelVersion: result.modelVersion,
promptVersion: result.promptVersion,
latencyMs: result.latencyMs,
costUsd: result.costUsd,
inputTokens: result.inputTokens,
outputTokens: result.outputTokens,
consentFlags,
readUrl: ctx.readUrl,
});
if (!capture.ok) {
console.error("[scan processor] shadow-capture misslyckades:", capture.error);
}
}
// Bokför verklig AI-kostnad/tokens utan PII (spec §45).
if (result.costUsd != null || result.inputTokens || result.outputTokens) {
await recordAiUsage(ctx.db, job.userId, {
@@ -107,7 +130,6 @@ export async function processScanJob(ctx: WorkerContext, scanJobId: string): Pro
},
});
}
void consents;
}
async function runAamosForJob(
@@ -117,6 +139,7 @@ async function runAamosForJob(
imageUrls: string[],
context: unknown,
localeContext: LocaleContext,
consentFlags: CaptureConsentFlags,
) {
switch (jobType) {
case "ANALYZE_FRIDGE_IMAGE":
@@ -129,33 +152,33 @@ async function runAamosForJob(
marketLocale: localeContext.languageTag,
knownItems: [],
},
{ localeContext },
{ localeContext, consentFlags },
);
case "ANALYZE_MEAL_IMAGE": {
const recipeContext = await buildRecipeContext(ctx, context);
return ctx.aamos.runTask(
"ANALYZE_MEAL_IMAGE",
{ imageUrls, recipeContext, marketLocale: localeContext.languageTag },
{ localeContext },
{ localeContext, consentFlags },
);
}
case "READ_RECEIPT":
return ctx.aamos.runTask(
"READ_RECEIPT",
{ imageUrls, marketLocale: localeContext.languageTag },
{ localeContext },
{ localeContext, consentFlags },
);
case "READ_NUTRITION_LABEL":
return ctx.aamos.runTask(
"READ_NUTRITION_LABEL",
{ imageUrls, marketLocale: localeContext.languageTag },
{ localeContext },
{ localeContext, consentFlags },
);
case "READ_EXPIRY_DATE":
return ctx.aamos.runTask(
"READ_EXPIRY_DATE",
{ imageUrls: imageUrls.slice(0, 2) },
{ localeContext },
{ localeContext, consentFlags },
);
default:
throw new Error(`Jobbtypen ${jobType} hanteras inte av scan-processorn`);