docs: add API ML service migration spec

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Asep Haryana Saputra
2026-05-22 19:46:48 +00:00
co-authored by Claude Opus 4.7
parent c3e7453390
commit b69b32f5b3
@@ -0,0 +1,101 @@
# API ML Service Migration Design
## Summary
Migrate `apps/api` image classification from in-process TensorFlow.js inference to the separate FastAPI ML service at `apps/ml-service`. The public API behavior and database writes stay the same; only the model inference boundary changes.
## Goals
- Remove TensorFlow.js model loading and image decoding from the Bun/Elysia API runtime.
- Keep existing route behavior for image classifications and diagnoses.
- Use `apps/ml-service` as the single runtime owner for Keras/TensorFlow inference.
- Preserve the internal `classifyImage(file)` API shape used by existing routes.
## Non-goals
- Do not change frontend behavior.
- Do not change database schema.
- Do not add a fallback to TFJS inference.
- Do not migrate upload storage or diagnosis workflows beyond the inference call.
## Architecture
`apps/api/src/lib/image-model.ts` remains the API-side classification boundary. Its implementation changes from local TFJS inference to an HTTP client for the ML service.
`classifyImage(file)` will:
1. Build multipart `FormData` with field name `file`.
2. POST it to `${env.mlServiceUrl}/predict`.
3. Validate the ML service response.
4. Map returned Indonesian labels to API disease slugs.
5. Return the existing `ClassificationResult` shape:
- `predictedDiseaseSlug`
- `confidence`
- `probabilities`
Existing route files continue to call `classifyImage(file)`, so route and database logic remain stable.
## Runtime configuration
Add `mlServiceUrl` to `apps/api/src/config/env.ts`.
Default local value:
```txt
http://127.0.0.1:8001
```
Environment variable:
```txt
ML_SERVICE_URL=http://127.0.0.1:8001
```
The API should trim trailing slashes before appending `/predict` so both `http://127.0.0.1:8001` and `http://127.0.0.1:8001/` work.
## Label mapping
The ML service returns labels. The API maps labels to catalog slugs:
- `Bercak Daun``bercak-daun`
- `Daun Sehat``daun-sehat`
- `Karat Daun``karat-daun`
- `Hawar Daun``hawar-daun`
This mapping follows the trained model output order handled by the ML service while preserving API/catalog slugs.
## Error handling
`classifyImage(file)` throws `Error` when:
- The input file MIME type is not `image/jpeg` or `image/png`.
- The ML service cannot be reached.
- The ML service returns a non-2xx response.
- The ML service response is malformed.
- The ML service returns a label that does not map to a known disease slug.
Existing routes keep their current behavior:
- `POST /api/v1/classifications/image` catches the error and returns service unavailable with `Model service error: ...`.
- `POST /api/v1/diagnoses` catches the error and stores a failed diagnosis record.
## Dependency cleanup
Remove API dependencies that only supported local TFJS inference:
- `@tensorflow/tfjs`
- `jpeg-js`
- `pngjs`
Keep dependencies needed by routes, database, auth, and upload unchanged.
## Verification
Required verification after implementation:
- Run API typecheck with `bun run typecheck` from the repository root.
- Confirm no `@tensorflow/tfjs`, `jpeg-js`, or `pngjs` imports remain under `apps/api/src`.
- Start `apps/ml-service` and confirm `GET /health` returns `model_loaded: true` when the Keras model exists.
- Exercise `classifyImage(file)` against the running ML service with a local image file or synthetic image and confirm it returns `predictedDiseaseSlug`, `confidence`, and sorted probabilities.
If full API route testing is blocked by external database or upload service requirements, report that explicitly and include the lower-level verification evidence.