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:
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user