Spaces:
Runtime error
Runtime error
fix: DICOM-aware vision prep with fallback degradation
Browse files
app/llm/stages/stage1_diagnostic.py
CHANGED
|
@@ -48,11 +48,35 @@ def _fallback_reason(exc: Exception) -> str:
|
|
| 48 |
return "llm_error"
|
| 49 |
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
def _encode_image_b64(image_path: Path) -> str:
|
| 52 |
"""Downscale to <= MAX_IMAGE_LONG_EDGE on the long edge, re-encode JPEG in memory."""
|
| 53 |
from PIL import Image
|
| 54 |
|
| 55 |
-
with
|
| 56 |
image = opened.copy() if opened.mode in ("L", "RGB") else opened.convert("RGB")
|
| 57 |
long_edge = max(image.size)
|
| 58 |
if long_edge > MAX_IMAGE_LONG_EDGE:
|
|
@@ -101,7 +125,12 @@ def generate_diagnostic_report(
|
|
| 101 |
if not llm_client.llm_available(settings):
|
| 102 |
raise llm_client.LLMUnavailableError("anthropic API key missing or SDK not installed")
|
| 103 |
system = _format_system(prompt.text, analysis)
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
context = (
|
| 106 |
f"Claim #{claim_id}. Declared modality: {declared_modality or 'not declared'}. "
|
| 107 |
f"Original upload media type: {image_media_type}. "
|
|
|
|
| 48 |
return "llm_error"
|
| 49 |
|
| 50 |
|
| 51 |
+
def _load_image(image_path: Path):
|
| 52 |
+
"""Open an upload for vision encoding; DICOM studies render via their pixel data."""
|
| 53 |
+
from PIL import Image
|
| 54 |
+
|
| 55 |
+
with image_path.open("rb") as fh:
|
| 56 |
+
head = fh.read(132)
|
| 57 |
+
is_dicom = (len(head) >= 132 and head[128:132] == b"DICM") or image_path.suffix.lower() in (
|
| 58 |
+
".dcm",
|
| 59 |
+
".dicom",
|
| 60 |
+
)
|
| 61 |
+
if is_dicom:
|
| 62 |
+
import pydicom
|
| 63 |
+
|
| 64 |
+
ds = pydicom.dcmread(image_path, force=True)
|
| 65 |
+
arr = ds.pixel_array.astype("float32")
|
| 66 |
+
lo, hi = float(arr.min()), float(arr.max())
|
| 67 |
+
arr = (arr - lo) / (hi - lo) * 255.0 if hi > lo else arr * 0.0
|
| 68 |
+
arr8 = arr.astype("uint8")
|
| 69 |
+
if arr8.ndim == 3: # color or multi-frame: first plane
|
| 70 |
+
arr8 = arr8[..., 0] if arr8.shape[-1] in (3, 4) else arr8[0]
|
| 71 |
+
return Image.fromarray(arr8)
|
| 72 |
+
return Image.open(image_path)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
def _encode_image_b64(image_path: Path) -> str:
|
| 76 |
"""Downscale to <= MAX_IMAGE_LONG_EDGE on the long edge, re-encode JPEG in memory."""
|
| 77 |
from PIL import Image
|
| 78 |
|
| 79 |
+
with _load_image(image_path) as opened:
|
| 80 |
image = opened.copy() if opened.mode in ("L", "RGB") else opened.convert("RGB")
|
| 81 |
long_edge = max(image.size)
|
| 82 |
if long_edge > MAX_IMAGE_LONG_EDGE:
|
|
|
|
| 125 |
if not llm_client.llm_available(settings):
|
| 126 |
raise llm_client.LLMUnavailableError("anthropic API key missing or SDK not installed")
|
| 127 |
system = _format_system(prompt.text, analysis)
|
| 128 |
+
try:
|
| 129 |
+
image_b64 = _encode_image_b64(image_path)
|
| 130 |
+
except Exception as exc:
|
| 131 |
+
# Whatever the upload turned out to be, vision prep must degrade to
|
| 132 |
+
# the deterministic fallback, never fail the stage.
|
| 133 |
+
raise llm_client.LLMUnavailableError(f"image preparation failed: {exc}") from exc
|
| 134 |
context = (
|
| 135 |
f"Claim #{claim_id}. Declared modality: {declared_modality or 'not declared'}. "
|
| 136 |
f"Original upload media type: {image_media_type}. "
|