Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,169 +1,263 @@
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
"""
|
3 |
-
app.py β
|
4 |
-
|
5 |
-
|
|
|
|
|
|
|
|
|
6 |
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
β’Β action_handlers.py β runs Gemini, UMLS enrichment, PDF reporting
|
11 |
-
|
12 |
-
Heavy logic lives in the helper modules; this file focuses on flow control.
|
13 |
"""
|
14 |
from __future__ import annotations
|
15 |
|
16 |
-
#
|
17 |
-
# StandardΒ library
|
18 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
19 |
import logging
|
20 |
import sys
|
21 |
import io
|
22 |
import base64
|
23 |
-
|
|
|
24 |
|
25 |
-
#
|
26 |
-
# 3rdβparty
|
27 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
28 |
import streamlit as st
|
|
|
29 |
|
30 |
-
# Pillow is
|
31 |
try:
|
32 |
-
from PIL import Image
|
33 |
PIL_AVAILABLE = True
|
34 |
-
except ImportError:
|
35 |
PIL_AVAILABLE = False
|
36 |
-
Image = None
|
|
|
|
|
|
|
|
|
37 |
|
38 |
-
#
|
39 |
-
#
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
from session_state import initialize_session_state
|
43 |
from sidebar_ui import render_sidebar
|
44 |
from main_page_ui import render_main_content
|
45 |
from file_processing import handle_file_upload
|
46 |
from action_handlers import handle_action
|
47 |
|
48 |
-
#
|
49 |
-
|
50 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
st.set_page_config(
|
52 |
-
page_title=
|
53 |
-
page_icon=
|
54 |
layout="wide",
|
55 |
initial_sidebar_state="expanded",
|
56 |
)
|
57 |
|
58 |
-
#
|
59 |
-
# Logging
|
60 |
-
#
|
61 |
-
|
62 |
-
|
|
|
63 |
|
|
|
64 |
logging.basicConfig(
|
65 |
level=LOG_LEVEL,
|
66 |
format=LOG_FORMAT,
|
67 |
datefmt=DATE_FORMAT,
|
68 |
-
stream=sys.stdout,
|
|
|
69 |
)
|
70 |
logger = logging.getLogger(__name__)
|
71 |
-
logger.info("
|
72 |
|
73 |
-
#
|
74 |
-
# Session
|
75 |
-
#
|
76 |
-
initialize_session_state()
|
|
|
77 |
|
78 |
-
#
|
79 |
-
#
|
80 |
-
#
|
81 |
st.markdown(APP_CSS, unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
|
83 |
-
#
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
if channels == "RGB" and img_obj.mode not in {"RGB", "L"}:
|
115 |
-
img_obj = img_obj.convert("RGB")
|
116 |
-
|
117 |
-
# ---------- encode ----------
|
118 |
-
buf = io.BytesIO()
|
119 |
-
img_obj.save(buf, format=fmt)
|
120 |
-
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
|
121 |
-
return f"data:image/{fmt.lower()};base64,{b64}"
|
122 |
-
|
123 |
-
_st_image.image_to_url = _image_to_url_monkey_patch # type: ignore[attr-defined]
|
124 |
-
logger.info("βοΈ Patched st.image βΒ dataβURL encoder")
|
125 |
-
|
126 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
127 |
-
# UIΒ flow
|
128 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
129 |
-
# 1οΈβ£ Sidebar (upload, actions) β returns fileβuploader object
|
130 |
-
uploaded_file = render_sidebar()
|
131 |
-
|
132 |
-
# 2οΈβ£ Process uploaded file (populate sessionβstate images / DICOM data)
|
133 |
handle_file_upload(uploaded_file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
|
135 |
-
#
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
5. Explore tabs βΒ **Translate**, **UMLS**, **Confidence** β¦
|
151 |
-
6. **Generate PDF** for a portable report.
|
152 |
-
""",
|
153 |
-
)
|
154 |
-
st.markdown("---")
|
155 |
-
|
156 |
-
col1, col2 = st.columns([2, 3], gap="large")
|
157 |
-
render_main_content(col1, col2)
|
158 |
-
|
159 |
-
# 4οΈβ£ Perform deferred action (set via sidebar buttons)
|
160 |
-
if (action := st.session_state.get("last_action")):
|
161 |
handle_action(action)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
|
163 |
-
#
|
164 |
-
#
|
165 |
-
#
|
166 |
-
|
167 |
-
st.caption(f"
|
168 |
st.markdown(FOOTER_MARKDOWN, unsafe_allow_html=True)
|
169 |
-
logger.info("
|
|
|
|
|
|
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
"""
|
3 |
+
app.py β RadVision AI Advanced (main entryβpoint)
|
4 |
+
-------------------------------------------------
|
5 |
+
Splitβarchitecture version that wires together:
|
6 |
+
β’ sidebar_ui.py β upload & action buttons
|
7 |
+
β’ main_page_ui.py β viewer + tabbed results
|
8 |
+
β’ file_processing.py β image / DICOM ingestion
|
9 |
+
β’ action_handlers.py β runs AI, UMLS, report
|
10 |
|
11 |
+
This file primarily orchestrates the application flow and displays
|
12 |
+
highβlevel status banners (e.g., missing optional dependencies).
|
13 |
+
Heavy logic resides within the imported helper modules.
|
|
|
|
|
|
|
14 |
"""
|
15 |
from __future__ import annotations
|
16 |
|
17 |
+
# Standard library imports should come first
|
|
|
|
|
18 |
import logging
|
19 |
import sys
|
20 |
import io
|
21 |
import base64
|
22 |
+
import os
|
23 |
+
from typing import Any, TYPE_CHECKING
|
24 |
|
25 |
+
# Third-party imports
|
|
|
|
|
26 |
import streamlit as st
|
27 |
+
from streamlit.runtime.scriptrunner import get_script_run_ctx # For reliable session ID
|
28 |
|
29 |
+
# Pillow is checked for availability, crucial for the monkey-patch.
|
30 |
try:
|
31 |
+
from PIL import Image
|
32 |
PIL_AVAILABLE = True
|
33 |
+
except ImportError:
|
34 |
PIL_AVAILABLE = False
|
35 |
+
Image = None # type: ignore[assignment, misc] # Define Image as None if Pillow isn't installed
|
36 |
+
|
37 |
+
# Conditional import for type checking improves static analysis
|
38 |
+
if TYPE_CHECKING:
|
39 |
+
from streamlit.runtime.uploaded_file_manager import UploadedFile
|
40 |
|
41 |
+
# Local application/library specific imports
|
42 |
+
# Configuration should be imported early
|
43 |
+
from config import (
|
44 |
+
LOG_LEVEL,
|
45 |
+
LOG_FORMAT,
|
46 |
+
DATE_FORMAT,
|
47 |
+
APP_CSS,
|
48 |
+
FOOTER_MARKDOWN,
|
49 |
+
APP_TITLE,
|
50 |
+
APP_ICON,
|
51 |
+
USER_GUIDE_MARKDOWN, # Assuming user guide text is moved to config
|
52 |
+
DISCLAIMER_WARNING, # Assuming disclaimer is moved to config
|
53 |
+
)
|
54 |
+
# Core application modules
|
55 |
from session_state import initialize_session_state
|
56 |
from sidebar_ui import render_sidebar
|
57 |
from main_page_ui import render_main_content
|
58 |
from file_processing import handle_file_upload
|
59 |
from action_handlers import handle_action
|
60 |
|
61 |
+
# Optional helpers (check availability without crashing)
|
62 |
+
try:
|
63 |
+
# Assuming translation_models exports this constant
|
64 |
+
from translation_models import TRANSLATION_AVAILABLE
|
65 |
+
except ImportError:
|
66 |
+
TRANSLATION_AVAILABLE = False
|
67 |
+
|
68 |
+
try:
|
69 |
+
# Assuming umls_utils exports this constant
|
70 |
+
from umls_utils import UMLS_AVAILABLE, UMLS_CONFIG_MSG # Add message for clarity
|
71 |
+
except ImportError:
|
72 |
+
UMLS_AVAILABLE = False
|
73 |
+
# Provide a default message if the module itself is missing
|
74 |
+
UMLS_CONFIG_MSG = "Add `UMLS_APIKEY` to HF Secrets & restart."
|
75 |
+
|
76 |
+
|
77 |
+
# ---------------------------------------------------------------------------
|
78 |
+
# Helper Functions
|
79 |
+
# ---------------------------------------------------------------------------
|
80 |
+
def get_session_id() -> str:
|
81 |
+
"""Retrieves the current Streamlit session ID reliably."""
|
82 |
+
ctx = get_script_run_ctx()
|
83 |
+
return ctx.session_id if ctx else "N/A"
|
84 |
+
|
85 |
+
# ---------------------------------------------------------------------------
|
86 |
+
# Streamlit Page Configuration (Must be the *first* Streamlit command)
|
87 |
+
# ---------------------------------------------------------------------------
|
88 |
st.set_page_config(
|
89 |
+
page_title=APP_TITLE,
|
90 |
+
page_icon=APP_ICON,
|
91 |
layout="wide",
|
92 |
initial_sidebar_state="expanded",
|
93 |
)
|
94 |
|
95 |
+
# ---------------------------------------------------------------------------
|
96 |
+
# Logging Configuration (Clear existing handlers, set new format)
|
97 |
+
# ---------------------------------------------------------------------------
|
98 |
+
# Remove default Streamlit handlers to prevent duplicate logs
|
99 |
+
for handler in logging.root.handlers[:]:
|
100 |
+
logging.root.removeHandler(handler)
|
101 |
|
102 |
+
# Configure root logger
|
103 |
logging.basicConfig(
|
104 |
level=LOG_LEVEL,
|
105 |
format=LOG_FORMAT,
|
106 |
datefmt=DATE_FORMAT,
|
107 |
+
stream=sys.stdout, # Log to stdout for containerized environments
|
108 |
+
force=True # Override any existing configuration
|
109 |
)
|
110 |
logger = logging.getLogger(__name__)
|
111 |
+
logger.info("--- RadVision AI App Initializing (Streamlit v%s) ---", st.__version__)
|
112 |
|
113 |
+
# ---------------------------------------------------------------------------
|
114 |
+
# Session State Initialization (Ensure it runs early)
|
115 |
+
# ---------------------------------------------------------------------------
|
116 |
+
initialize_session_state(get_session_id()) # Pass session ID during init if needed
|
117 |
+
logger.debug("Session state initialized.")
|
118 |
|
119 |
+
# ---------------------------------------------------------------------------
|
120 |
+
# Apply Global CSS Styles
|
121 |
+
# ---------------------------------------------------------------------------
|
122 |
st.markdown(APP_CSS, unsafe_allow_html=True)
|
123 |
+
logger.debug("Global CSS applied.")
|
124 |
+
|
125 |
+
# ---------------------------------------------------------------------------
|
126 |
+
# Monkey-Patch for st.image (if needed and Pillow is available)
|
127 |
+
# Required for st_canvas snapshot rendering compatibility
|
128 |
+
# ---------------------------------------------------------------------------
|
129 |
+
import streamlit.elements.image as st_image # Use alias for clarity
|
130 |
+
|
131 |
+
# Check if the function *already exists* (e.g., future Streamlit versions)
|
132 |
+
if not hasattr(st_image, "image_to_url"):
|
133 |
+
if not PIL_AVAILABLE:
|
134 |
+
logger.warning("Pillow library not found. Cannot apply image_to_url monkey-patch.")
|
135 |
+
else:
|
136 |
+
logger.info("Applying monkey-patch for st.image -> data-url generation.")
|
137 |
+
def _image_to_url_monkey_patch(
|
138 |
+
image: Any, # Can be various types, PIL Image is key
|
139 |
+
width: int = -1,
|
140 |
+
clamp: bool = False,
|
141 |
+
channels: str = "RGB",
|
142 |
+
output_format: str = "auto",
|
143 |
+
image_id: str = "", # May be used by Streamlit internally
|
144 |
+
) -> str:
|
145 |
+
"""
|
146 |
+
Serializes PIL Image to a base64 data URL.
|
147 |
+
Needed for components like streamlit-drawable-canvas to redisplay images.
|
148 |
+
Handles basic format conversions (e.g., palette to RGBA, alpha channel).
|
149 |
+
"""
|
150 |
+
# Ensure input is a PIL Image object
|
151 |
+
if not isinstance(image, Image.Image):
|
152 |
+
logger.warning("image_to_url: Input is not a PIL Image (%s). Returning empty URL.", type(image))
|
153 |
+
return ""
|
154 |
+
|
155 |
+
# Determine output format, defaulting to PNG
|
156 |
+
fmt = output_format.upper() if output_format != "auto" else (image.format or "PNG")
|
157 |
+
if fmt not in {"PNG", "JPEG", "WEBP", "GIF"}:
|
158 |
+
logger.debug("image_to_url: Unsupported format '%s', falling back to PNG.", fmt)
|
159 |
+
fmt = "PNG"
|
160 |
+
|
161 |
+
# Handle alpha channel compatibility (JPEG doesn't support alpha)
|
162 |
+
if image.mode == "RGBA" and fmt == "JPEG":
|
163 |
+
logger.debug("image_to_url: RGBA image requested as JPEG, converting to PNG.")
|
164 |
+
fmt = "PNG" # Switch to PNG to preserve alpha
|
165 |
+
|
166 |
+
# Convert palette images (mode 'P') to RGBA for broader compatibility
|
167 |
+
if image.mode == "P":
|
168 |
+
logger.debug("image_to_url: Converting Palette image (mode 'P') to RGBA.")
|
169 |
+
image = image.convert("RGBA")
|
170 |
+
|
171 |
+
# Convert to RGB if requested and necessary
|
172 |
+
if channels == "RGB" and image.mode not in {"RGB", "L"}: # 'L' (grayscale) is fine
|
173 |
+
logger.debug("image_to_url: Converting image mode '%s' to RGB.", image.mode)
|
174 |
+
image = image.convert("RGB")
|
175 |
|
176 |
+
# Save image to an in-memory buffer
|
177 |
+
buffer = io.BytesIO()
|
178 |
+
try:
|
179 |
+
image.save(buffer, format=fmt)
|
180 |
+
img_data = buffer.getvalue()
|
181 |
+
except Exception as e:
|
182 |
+
logger.error("image_to_url: Failed to save image to buffer (format: %s): %s", fmt, e)
|
183 |
+
return ""
|
184 |
+
|
185 |
+
# Encode buffer to base64 and create data URL
|
186 |
+
b64_data = base64.b64encode(img_data).decode("utf-8")
|
187 |
+
return f"data:image/{fmt.lower()};base64,{b64_data}"
|
188 |
+
|
189 |
+
# Apply the patch
|
190 |
+
st_image.image_to_url = _image_to_url_monkey_patch # type: ignore[attr-defined]
|
191 |
+
logger.info("Monkey-patch applied successfully.")
|
192 |
+
else:
|
193 |
+
logger.info("Streamlit version >= X.Y.Z already has image_to_url. Skipping monkey-patch.")
|
194 |
+
|
195 |
+
|
196 |
+
# ---------------------------------------------------------------------------
|
197 |
+
# === Main Application Flow ===
|
198 |
+
# ---------------------------------------------------------------------------
|
199 |
+
|
200 |
+
# --- 1. Render Sidebar & Get Uploaded File ---
|
201 |
+
# The sidebar function handles its own elements and returns the file object
|
202 |
+
uploaded_file: UploadedFile | None = render_sidebar()
|
203 |
+
logger.debug("Sidebar rendered. Uploaded file: %s", uploaded_file.name if uploaded_file else "None")
|
204 |
+
|
205 |
+
# --- 2. Process Uploaded File ---
|
206 |
+
# This function updates session state with the processed image data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
handle_file_upload(uploaded_file)
|
208 |
+
# Logging within handle_file_upload is assumed
|
209 |
+
|
210 |
+
# --- 3. Render Main Content Area ---
|
211 |
+
# Title, User Guide Expander, Columns, Viewer, Results Tabs
|
212 |
+
st.divider() # Use st.divider() for modern separator
|
213 |
+
st.title(f"{APP_ICON} {APP_TITLE} Β· AI-Assisted Image Analysis")
|
214 |
+
|
215 |
+
with st.expander("User Guide & Disclaimer", expanded=False):
|
216 |
+
st.warning(f"β οΈ **Disclaimer**: {DISCLAIMER_WARNING}")
|
217 |
+
st.markdown(USER_GUIDE_MARKDOWN, unsafe_allow_html=True) # Assuming markdown format
|
218 |
|
219 |
+
st.divider() # Use st.divider()
|
220 |
+
|
221 |
+
# Define layout columns
|
222 |
+
col_img_viewer, col_analysis_results = st.columns([2, 3], gap="large") # Use descriptive names
|
223 |
+
|
224 |
+
# Render the main page UI components into the columns
|
225 |
+
render_main_content(col_img_viewer, col_analysis_results)
|
226 |
+
logger.debug("Main content area rendered.")
|
227 |
+
|
228 |
+
# --- 4. Handle Deferred Actions ---
|
229 |
+
# Check if an action button was clicked (state set in sidebar_ui or action_handlers)
|
230 |
+
action = st.session_state.get("last_action")
|
231 |
+
if action:
|
232 |
+
logger.info("Executing deferred action: '%s'", action)
|
233 |
+
# Action handler manages its own logic, spinners, and state updates
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
234 |
handle_action(action)
|
235 |
+
# Reset the action trigger ONLY if the handler didn't redirect/rerun
|
236 |
+
if st.session_state.get("last_action") == action: # Check if state changed during handler
|
237 |
+
st.session_state.last_action = None # Prevent re-triggering on next rerun
|
238 |
+
logger.debug("Action '%s' completed and reset.", action)
|
239 |
+
else:
|
240 |
+
logger.debug("No deferred action pending.")
|
241 |
+
|
242 |
+
|
243 |
+
# --- 5. Display Status Banners for Optional Features ---
|
244 |
+
# These appear near the bottom, informed by the availability flags
|
245 |
+
if not TRANSLATION_AVAILABLE:
|
246 |
+
st.warning("π Translation backend not loaded β install `deep-translator` & restart.")
|
247 |
+
logger.warning("Optional feature unavailable: Translation (deep-translator not found).")
|
248 |
+
|
249 |
+
if not UMLS_AVAILABLE:
|
250 |
+
st.warning(f"𧬠UMLS features unavailable β {UMLS_CONFIG_MSG}")
|
251 |
+
logger.warning("Optional feature unavailable: UMLS (%s)", UMLS_CONFIG_MSG)
|
252 |
+
|
253 |
|
254 |
+
# --- 6. Render Footer ---
|
255 |
+
st.divider() # Use st.divider()
|
256 |
+
# Use the reliable session ID getter
|
257 |
+
current_session_id = get_session_id()
|
258 |
+
st.caption(f"{APP_ICON} {APP_TITLE} | Session ID: {current_session_id}")
|
259 |
st.markdown(FOOTER_MARKDOWN, unsafe_allow_html=True)
|
260 |
+
logger.info("--- Render cycle complete β Session ID: %s ---", current_session_id)
|
261 |
+
# ---------------------------------------------------------------------------
|
262 |
+
# === End of Application Flow ===
|
263 |
+
# ---------------------------------------------------------------------------
|