update the codebase poc ver1

This commit is contained in:
DatTT127
2026-07-07 15:54:17 +07:00
parent fed5f277f4
commit 1622dc8fc5
452 changed files with 83999 additions and 66328 deletions

View File

@@ -0,0 +1,174 @@
# ML inference cache & thundering-herd control
PoC design for reusing segmentation / angle / inflammation results across **page refresh**, **multiple browser tabs**, and **duplicate UI triggers** — without re-calling Modal Triton for the same profile.
## Problem
| Trigger | Before cache |
|---------|----------------|
| Open clinical workspace (3 frames) | 2 proxy HTTP + ~9 Triton calls |
| Refresh page | Full repeat (in-memory cache lost) |
| Second tab, same patient | Full repeat (separate JS heap) |
| React Strict Mode double-mount | Partially deduped in one tab only |
| Identical batch POST while first in flight | Proxy runs work again (queue only) |
## Cache identity (invariants)
A cached result is valid when all of these match:
```
patientMrn — e.g. BN-2024-1847 (encounter / patient scope)
frameId — e.g. med-lat-1
contentHash — SHA-256 of frame image bytes (invalidates on re-scan)
pipelineVersion — model + postprocess fingerprint (invalidates on deploy)
```
**Cache key string:**
```
{patientMrn}|{frameId}|{contentHash}|{pipelineVersion}
```
**Batch coordination key** (cross-tab single-flight):
```
{patientMrn}|{pipelineVersion}|seg:{angleType}:{sortedFrameIds}
{patientMrn}|{pipelineVersion}|angle:{sortedFrameIds}
```
## CV pipeline (spec §7)
Frontend calls **`POST /api/test/analyze/batch`** — one orchestrated path per frame:
```
CLAHE → angle classification → (post-trans | sup-up-long only) inflammation
→ if inflammation: segmentation + thickness + severity + overlay
→ else: severity level 0, segmentation skipped
→ other angles: angle + severity 0 only
```
Pipeline version: **`poc-v2-spec-cv`** (`ML_INFERENCE_PIPELINE_VERSION` / `PROXY_PIPELINE_VERSION`).
Legacy split endpoints (`/segment/batch`, `/angle/batch`) remain for debugging but bypass spec gating.
## Two layers
### Layer 1 — IndexedDB (persist + refresh)
- Database: `lumina-msk-ml` / store `frame-inference-cache`
- Survives refresh and is **shared across tabs** on the same origin
- TTL: 24 hours (`cachedAt`); stale entries ignored
- Stores per frame:
- segmentation: `overlaySrc`, `raw` backend payload subset
- angle: `AngleClassificationResult`
- inflammation is embedded in segmentation `raw.inflammation`
### Layer 2 — BroadcastChannel (thundering herd)
- Channel: `lumina-ml-inflight`
- Messages: `fetch-start` / `fetch-done` / `fetch-error` with `batchKey`
- Tab that starts a network batch announces `fetch-start`
- Other tabs **await** `fetch-done` for the same `batchKey`, then read IndexedDB
- Fixes race where two cold tabs both miss IDB before either writes
In-tab `inflightBatchByKey` Map remains as a third line of defense.
## Frontend call flow
```
getSegmentationResultsForProfile(frames, { patientMrn })
1. For each frame: read IDB by cache key → hydrate memory cache
2. If all frames hit → return (0 HTTP)
3. batchKey = profile batch key
4. await crossTab.waitIfInflight(batchKey) // another tab may have finished
5. Re-check IDB + memory
6. in-flight Map single-flight → fetchSegmentationBatchForProfile
7. Write IDB per frame; crossTab.notifyDone(batchKey)
```
Angle batch follows the same pattern via `getAngleClassificationResultsForProfile`.
`useSegmentationOverlay` passes `patientMrn` from `ClinicalWorkspacePage``DiagnosticCanvas`.
## Proxy server cache (`test_fast_api_proxy.py`)
In-memory (process lifetime):
| Map | Purpose |
|-----|---------|
| `_proxy_result_cache` | `cache_key → (expires_monotonic, json-serializable result)` |
| `_proxy_inflight` | `cache_key → asyncio.Future` — coalesce identical batch requests |
**Segment batch key:**
```
segment|{angle_type}|{pipeline_version}|{sorted(frame_id:sha256(image_bytes))}
```
**Angle batch key:**
```
angle|{pipeline_version}|{sorted(frame_id:sha256(image_bytes))}
```
TTL default: 1 hour (`PROXY_RESULT_CACHE_TTL_S` env).
On cache hit: return cached JSON immediately (0 Triton).
On concurrent miss: later request awaits the first request's Future.
## Pipeline version
Frontend: `ML_INFERENCE_PIPELINE_VERSION` in `mlInferencePipelineVersion.ts`
Proxy: `PROXY_PIPELINE_VERSION` env (default `poc-v1`)
**Bump both** when Triton model names or overlay/postprocess logic change.
## Debugging
| Symptom | Check |
|---------|--------|
| Still 9 Triton calls every refresh | IDB miss — DevTools → Application → IndexedDB → `lumina-msk-ml` |
| Two tabs both fetch | BroadcastChannel not supported or different `patientMrn` / `pipelineVersion` |
| Stale overlay after model update | Bump `ML_INFERENCE_PIPELINE_VERSION` |
| Proxy logs "cache hit" | Working — no Triton for that batch |
| 502 herd | Separate issue — lock + retries; cache reduces frequency |
## Files
| File | Role |
|------|------|
| `docs/ml-inference-cache.md` | This document |
| `frontend/.../mlInferencePipelineVersion.ts` | Version constant |
| `frontend/.../mlInferenceCacheStore.ts` | IndexedDB read/write |
| `frontend/.../mlInferenceCrossTab.ts` | BroadcastChannel coordinator |
| `frontend/.../cvAnalyzeApi.ts` | Spec CV pipeline — single `/api/test/analyze/batch` call |
| `frontend/.../angleClassificationApi.ts` | IDB + cross-tab integration |
| `backend/tests/test_fast_api_proxy.py` | Server result cache + in-flight dedupe |
| `frontend/.../mlServiceError.ts` | Classify raw errors → ticket-friendly `MlServiceError` |
| `frontend/.../MlServiceErrorPanel.tsx` | Canvas overlay UI (support ref, copy, retry) |
## ML error UX (ticket-friendly)
When segmentation fails, the canvas shows **`MlServiceErrorPanel`** instead of raw browser messages (e.g. Safaris *“The string did not match the expected pattern”*).
| Audience | What they see |
|----------|----------------|
| Clinician | Reassuring title + plain Vietnamese explanation; ultrasound image unchanged |
| IT / ticket | **Mã tham chiếu** (`LUM-ML-…`) + **Sao chép cho ticket** button |
| Developer | Collapsed **Chi tiết kỹ thuật** — operation, frame id, raw message, remediation hint |
**Error codes:** `BAD_RESPONSE` (proxy down / HTML not JSON), `NETWORK`, `SERVER_OVERLOAD` (502/503/Triton), `SERVER_UNAVAILABLE`, `CLIENT_ERROR`, `NO_RESULT`, `UNKNOWN`.
**Debugging:** Ask user for support reference → match browser console Network tab (`/api/test/segment/batch`) with proxy logs. Common root cause for `BAD_RESPONSE`: `test_fast_api_proxy.py` not running on port 8001.
## Worklist AI grade
Home-screen **Đề xuất AI** never uses `MOCK_PATIENTS.synovitisGrade`.
| State | Card shows |
|-------|------------|
| No cached inference | `Chưa phân tích` |
| Reading IndexedDB | `Đang tải…` |
| After workspace ML run | `Độ X` — max `severity.level` across cached profile frames for that `patientMrn` |
Grades refresh when the worklist tab becomes visible again (return from clinical workspace).