cxr-report-generation / src /parsing.py
adhisetiawan's picture
Deploy CXR report generation demo
16d6749
Raw
History Blame Contribute Delete
5.86 kB
from __future__ import annotations
import json
import re
from typing import Any
_JSON_FENCE_PATTERN = re.compile(r"```json\s*(.*?)\s*```", re.IGNORECASE | re.DOTALL)
_CONSOLIDATED_FINDINGS_MARKER = "Consolidated list of findings:"
def _loads_first_json(candidate: str) -> Any:
decoder = json.JSONDecoder()
stripped = candidate.strip()
if not stripped:
raise ValueError("JSON candidate is empty.")
for index, char in enumerate(stripped):
if char not in "{[":
continue
try:
payload, _ = decoder.raw_decode(stripped, index)
return payload
except json.JSONDecodeError:
continue
raise ValueError("Could not parse a JSON value from the candidate text.")
def _scan_json_values(text: str) -> list[Any]:
decoder = json.JSONDecoder()
values: list[Any] = []
for index, char in enumerate(text):
if char not in "{[":
continue
try:
payload, _ = decoder.raw_decode(text, index)
except json.JSONDecodeError:
continue
values.append(payload)
return values
def _is_finding_item(value: Any) -> bool:
if not isinstance(value, dict):
return False
return all(key in value for key in ("finding", "anatomical_location", "certainty"))
def _is_finding_list(value: Any) -> bool:
if not isinstance(value, list):
return False
return all(_is_finding_item(item) for item in value)
def _normalize_discovery_candidate(payload: Any) -> dict[str, Any] | None:
if isinstance(payload, dict) and "findings" in payload:
if isinstance(payload["findings"], list):
return payload
return None
if _is_finding_list(payload):
return {"findings": payload}
if _is_finding_item(payload):
return None
return None
def _has_structured_discovery_payload(values: list[Any]) -> bool:
for value in values:
if isinstance(value, dict) and "findings" in value:
return True
if _is_finding_list(value):
return True
return False
def strip_medgemma_thinking_trace(response: str) -> str:
"""Remove the notebook-documented MedGemma thinking trace when present."""
if "<unused95>" in response:
return response.split("<unused95>", 1)[1].lstrip()
return response.strip()
def extract_finding_discovery_payload(response: str) -> dict[str, Any]:
cleaned = strip_medgemma_thinking_trace(response).strip()
scanned_values = _scan_json_values(cleaned)
standalone_finding_dicts = [
value for value in scanned_values if _is_finding_item(value)
]
if len(standalone_finding_dicts) >= 2 and not _has_structured_discovery_payload(
scanned_values
):
raise ValueError(
"Could not extract finding discovery JSON from MedGemma response. "
f"Response preview: {cleaned[:500]!r}"
)
try:
full_payload = json.loads(cleaned)
except json.JSONDecodeError:
full_payload = None
else:
normalized = _normalize_discovery_candidate(full_payload)
if normalized is not None:
return normalized
for value in scanned_values:
if isinstance(value, dict) and "findings" in value:
normalized = _normalize_discovery_candidate(value)
if normalized is not None:
return normalized
if _CONSOLIDATED_FINDINGS_MARKER in cleaned:
after_marker = cleaned.split(_CONSOLIDATED_FINDINGS_MARKER, 1)[1].strip()
try:
marker_payload = _loads_first_json(after_marker)
except ValueError:
marker_payload = None
else:
normalized = _normalize_discovery_candidate(marker_payload)
if normalized is not None:
return normalized
for match in _JSON_FENCE_PATTERN.finditer(cleaned):
fenced_text = match.group(1).strip()
try:
fenced_payload = _loads_first_json(fenced_text)
except ValueError:
continue
normalized = _normalize_discovery_candidate(fenced_payload)
if normalized is not None:
return normalized
for value in scanned_values:
if _is_finding_list(value):
return {"findings": value}
raise ValueError(
"Could not extract finding discovery JSON from MedGemma response. "
f"Response preview: {cleaned[:500]!r}"
)
def extract_json_payload(response: str, expected_type: type) -> Any:
cleaned = strip_medgemma_thinking_trace(response)
candidates: list[str] = [
match.group(1).strip()
for match in _JSON_FENCE_PATTERN.finditer(cleaned)
]
if not candidates:
if expected_type is dict:
start = cleaned.find("{")
end = cleaned.rfind("}")
elif expected_type is list:
start = cleaned.find("[")
end = cleaned.rfind("]")
else:
raise TypeError("expected_type must be dict or list.")
if start == -1 or end == -1 or end <= start:
raise ValueError("Could not locate a JSON payload in the model response.")
candidates.append(cleaned[start : end + 1])
errors: list[str] = []
for candidate in candidates:
try:
payload = _loads_first_json(candidate)
except ValueError as error:
errors.append(str(error))
continue
if isinstance(payload, expected_type):
return payload
errors.append(
f"expected {expected_type.__name__}, received {type(payload).__name__}"
)
detail = "; ".join(errors) if errors else "no JSON candidates were found"
raise ValueError(
f"Could not extract JSON {expected_type.__name__} from the model response: {detail}"
)