# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project

`palm_oil_mobile` is a Flutter app that runs on-device YOLO26 (NMS-free) inference to detect palm oil fresh fruit bunch (FFB) ripeness against MPOB standards, entirely offline (no server/network calls — contrast with `frontend/` and `server-desktop/` in the parent monorepo, which do remote inference over sockets). Detection classes: `Ripe`, `Unripe`, `Underripe`, `Overripe`, `Abnormal`, `Empty_Bunch`.

## Commands

```bash
flutter pub get                 # install dependencies
flutter run                     # run on connected device/emulator
flutter build apk                # release Android build
flutter test                     # run all tests
flutter test test/widget_test.dart   # run a single test file
flutter analyze                  # static analysis (flutter_lints, see analysis_options.yaml)
```

There is no backend to start — the model (`assets/best.tflite`) and labels (`assets/labels.txt`) are bundled assets loaded at runtime.

## Architecture

### Isolate-based inference pipeline

All TFLite inference lives in `lib/services/tflite_service.dart`. `TfliteService` spawns a single **persistent isolate** on `initModel()` (not one isolate per inference) that owns the `Interpreter`. The main isolate communicates via `SendPort`/`ReceivePort` message passing with commands `init`, `inference_static`, `inference_stream`. This keeps camera-stream frame processing off the UI thread. `isIsolateBusy` is used as a gate so callers never queue up a second in-flight inference against the same isolate.

Two inference paths decode differently before running the same model:
- **Static** (`_inferenceStaticTask`): decodes a file (from camera capture or gallery pick) via the `image` package, center-crops to square, resizes to 640×640.
- **Streaming** (`_inferenceStreamTask`): consumes raw `CameraImage` planes and manually converts YUV420 (Android) or BGRA8888 (iOS) to RGB before the same crop/resize — see `_convertYUV420ToImage` for the manual colorspace math.

Detection decoding (`_decodeDetections`) assumes the model is **NMS-free (YOLO26 end-to-end)** — output boxes are used directly without a secondary non-max-suppression pass in Dart. Confidence threshold is `0.25` (`_confidenceThreshold`); box coordinates come back normalized and are re-mapped from crop-space to full-frame-space when crop offsets are provided.

### "Point-and-scan" live capture state machine

`lib/screens/live_analysis_screen.dart` implements an auto-capture flow driven by a `DetectionState` enum (`searching → locking → capturing → cooldown`) rather than a manual shutter button:
- A 15-frame sliding window (`_detectionHistory`) tracks recent high-confidence hits; reaching `_requiredHits` (5) transitions `searching → locking`.
- While locking, a 100ms-tick momentum timer (`_startLockTimer`) accumulates/drains "momentum ticks" — sustained detection advances toward auto-capture, a lost target drains it back to `searching`.
- On lock, `_triggerCapture` stops the camera stream, takes a high-res still, re-runs inference on that still (the stream inference is only used for lock detection, not the persisted result), saves to history, then a 3s cooldown timer restarts the stream.

`static_capture_screen.dart` (manual shutter) and `analysis_screen.dart` (gallery picker) follow simpler capture → infer → persist → save flows without the state machine.

### Persistence

`lib/services/database_helper.dart` is a singleton wrapping a `sqflite` table `history`. Each row is a `PalmRecord` (`lib/models/palm_record.dart`): the *best* (highest-confidence) detection's box/class/confidence are stored as first-class columns, while the *full* detection list for that frame is serialized as a JSON blob in the `detections` column (bump `_onUpgrade`'s version check when changing schema). Captured images are copied out of the ephemeral camera/picker temp path into `getApplicationDocumentsDirectory()` before the DB row is written, so history entries survive app restarts.

### Shared UI

`lib/widgets/palm_bounding_box.dart` (`PalmBoundingBox`) is the canonical bounding-box overlay with the MPOB color palette (green=Ripe, orange/yellow=Underripe, blue=Unripe, red=Abnormal, gray=Empty_Bunch) and is reused across the live, gallery, and history-detail screens. `static_capture_screen.dart` still has its own local `_buildBoundingBox` — prefer `PalmBoundingBox` for any new box-drawing code.

### Screen navigation

`main.dart` → `HomeScreen` is a plain card-based `Navigator.push` menu into four independent screens (`AnalysisScreen`, `StaticCaptureScreen`, `LiveAnalysisScreen`, `HistoryScreen`) — no named routes, no shared state/provider layer. Each screen owns its own `TfliteService` and `DatabaseHelper` instance and must call `_tfliteService.dispose()` in `dispose()` to kill its isolate.
