# 05. Real Model Integration into Identification Pipeline meta: id: production-ml-pipeline-05 feature: production-ml-pipeline priority: P0 depends_on: [production-ml-pipeline-02, production-ml-pipeline-03, production-ml-pipeline-04] tags: [implementation, integration, tests-required] objective: - Wire the real TF.js model into the `/api/identify` endpoint - Replace demo/mock predictions with real model inference - Use the PlantVillage label mapping (task 02) to resolve class indices to disease IDs - Apply confidence calibration (task 04) to produce meaningful confidence scores - Remove the `demo_mode` fallback path - Handle healthy class predictions correctly (return "no disease detected" message) deliverables: - `src/app/api/identify/route.ts` — rewritten to use real model inference - `src/lib/ml/inference.ts` — updated to use calibration and return structured results - `src/lib/api/identify.ts` — client-side API updated for new response shape - `src/components/ResultsDashboard.tsx` — handle healthy predictions and remove demo mode badge - `src/components/HealthyResult.tsx` — new component for "no disease detected" state steps: 1. **Rewrite `/api/identify` route handler** to use real inference: ```typescript export async function POST(request: NextRequest) { // 1. Parse request, validate imageId // 2. Load and preprocess image (existing code) // 3. Run inference with real model const { probabilities, inferenceTimeMs } = await runInference(tensor); // 4. Calibrate confidence const calibrated = calibratePrediction(probabilities, isLogits); // 5. Map to disease using PlantVillage labels const diseaseId = getDiseaseIdForIndex(calibrated.classIndex); const isHealthy = isHealthyClass(calibrated.classIndex); // 6. If healthy, return healthy result if (isHealthy && calibrated.adjusted > 0.5) { return NextResponse.json({ healthy: true, plantId: getPlantIdForIndex(calibrated.classIndex), confidence: calibrated, metadata: { model: MODEL_ID, inferenceTimeMs, imageId }, }); } // 7. Get top-K predictions (not just top-1) const topK = getTopKFloat32(probabilities, 5); const predictions = await enrichPredictions(topK); // 8. Return results return NextResponse.json({ predictions, metadata: { model: MODEL_ID, inferenceTimeMs, imageId }, demo_mode: false, // or remove this field entirely }); } ``` 2. **Update `runInference()` to return calibrated results**: ```typescript export async function runInference( imageTensor: Float32Array, topK: number = 5, ): Promise { const model = await getModel(); const modelStatus = model.getStatus(); if (!modelStatus.loaded) { throw new Error("Model not loaded. Cannot run inference."); } const { output, inferenceTimeMs } = await model.predict(imageTensor); // Determine if output is logits or probabilities const isLogits = !isProbabilities(output); // Apply calibration const calibration = calibratePrediction(output, isLogits); // Get top-K predictions const probs = isLogits ? temperatureScaledSoftmax(output) : output; const topKPredictions = getTopKFloat32(probs, topK); return { predictions: topKPredictions, inferenceTimeMs, calibration: { temperature: PLANTVILLAGE_CALIBRATION.temperature, entropy: calibration.entropy, entropyConfidence: calibration.entropyConfidence, }, }; } function isProbabilities(output: Float32Array): boolean { const sum = output.reduce((a, b) => a + b, 0); return Math.abs(sum - 1.0) < 0.01; } ``` 3. **Update `enrichPredictions()` to use new label mapping**: ```typescript async function enrichPredictions( topPredictions: Array<{ classIndex: number; probability: number }>, ): Promise { const results: PredictionResult[] = []; for (const pred of topPredictions) { // Skip healthy classes in top-K (they're handled separately) if (isHealthyClass(pred.classIndex)) continue; const diseaseId = getDiseaseIdForIndex(pred.classIndex); const plantId = getPlantIdForIndex(pred.classIndex); if (!diseaseId || diseaseId === "healthy") continue; const disease = await getDiseaseById(diseaseId); if (!disease) continue; // Use probability as raw confidence, calibrate with entropy const confidence = calibrateConfidence(pred.probability); const plant = await getPlantById(disease.plantId).catch(() => null); results.push({ diseaseId, disease, confidence, lookalikes: disease.lookalikeDiseaseIds, plant: plant ?? null, }); } results.sort((a, b) => b.confidence.adjusted - a.confidence.adjusted); return results; } ``` 4. **Update response types** to support healthy result: ```typescript // src/lib/types.ts export interface IdentifyResponse { predictions?: PredictionResult[]; healthy?: boolean; plantId?: string; confidence?: ConfidenceResult; metadata: InferenceMetadata; demo_mode?: boolean; // Remove or always false } ``` 5. **Update `ResultsDashboard` component** to handle healthy result: ```tsx // If response.healthy === true, show HealthyResult component instead of prediction cards if (response?.healthy) { return ; } ``` 6. **Create `HealthyResult` component** `src/components/HealthyResult.tsx`: ```tsx export default function HealthyResult({ plantId, confidence }) { const plant = usePlant(plantId); // fetch plant data return (
🌿

No Disease Detected

The image appears healthy{plant ? ` (${plant.commonName})` : ""}. Confidence:{" "} {Math.round(confidence.adjusted * 100)}%

If symptoms persist, try uploading a clearer photo of the affected area.

); } ``` 7. **Remove demo mode logic**: - In `model-loader.ts`: remove `createMockModel()` fallback (or keep it but only for development) - In `route.ts`: remove `demo_mode: true` branch - In `ResultsDashboard.tsx`: remove "Demo mode" badge - In `src/lib/api/identify.ts`: remove `demo_mode` from response type 8. **Add error handling for model not loaded**: ```typescript const model = await getModel(); if (!model.getStatus().loaded) { return NextResponse.json( { error: "Model not available", message: "ML model failed to load. Please try again later.", }, { status: 503 }, ); } ``` 9. **Update client-side API** `src/lib/api/identify.ts`: ```typescript export interface IdentifyResponse { predictions?: PredictionResult[]; healthy?: boolean; plantId?: string; confidence?: ConfidenceResult; metadata: InferenceMetadata; } ``` 10. **Add structured logging** for inference requests: ```typescript console.log( JSON.stringify({ event: "inference", imageId, modelId: MODEL_ID, inferenceTimeMs, topPrediction: predictions[0]?.diseaseId, confidence: predictions[0]?.confidence.adjusted, entropy: calibration?.entropy, }), ); ``` tests: - Integration: POST `/api/identify` with valid imageId returns real predictions (no `demo_mode: true`) - Integration: response includes `predictions` array with valid diseaseIds from KB - Integration: confidence scores are calibrated (not raw softmax) - Integration: healthy predictions return `healthy: true` with plantId - Unit: `enrichPredictions()` skips healthy classes in top-K - Unit: `isProbabilities()` correctly identifies probability output - Unit: `runInference()` throws error if model not loaded - E2E: upload a tomato leaf image → get tomato disease predictions - E2E: upload a healthy plant image → get healthy result acceptance_criteria: - `/api/identify` returns real model predictions (not mock) - All diseaseIds in response are valid KB entries (verifiable via `getDiseaseById()`) - Confidence scores use temperature-scaled calibration (not raw softmax) - Healthy predictions return `{ healthy: true, plantId, confidence }` instead of disease predictions - Demo mode is completely removed from production path - Error handling: model not loaded → 503 response with clear message - Structured logging for every inference request - Client-side API handles new response shape (healthy vs predictions) validation: - `npx vitest run src/app/api/identify/identify.test.ts` - `npx vitest run src/lib/ml/inference.test.ts` - `curl -X POST http://localhost:3000/api/identify -H "Content-Type: application/json" -d '{"imageId":""}'` — response has real predictions - Upload a test image via UI → see real disease names (not demo mode) - Check server logs: `event: "inference"` with valid modelId and inferenceTimeMs notes: - This task depends on tasks 02, 03, and 04 being complete. Do not start until all dependencies are met. - The `enrichPredictions()` function now skips healthy classes — they're handled by the healthy result path - If the model is not loaded, return 503 (Service Unavailable) instead of falling back to mock - Structured logging should be JSON for easy parsing by log aggregators - The `demo_mode` field can be removed entirely or kept as `false` for backwards compatibility