mgbam commited on
Commit
f1fe858
Β·
verified Β·
1 Parent(s): 8bba2ee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +219 -125
app.py CHANGED
@@ -1,169 +1,263 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- app.py – RadVisionΒ AIΒ Advanced (main entry‑point)
4
- ================================================
5
- Lightweight orchestrator that wires together:
 
 
 
 
6
 
7
- β€’Β sidebar_ui.py – upload panel & action buttons
8
- β€’Β main_page_ui.py – viewer + tabbed results (AI / UMLS / Translate …)
9
- β€’Β file_processing.py – DICOM / image ingestion + ROI plumbing
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
- from typing import Any
 
24
 
25
- # ──────────────────────────────────────────────────────────────────────────────
26
- # 3rd‑party
27
- # ──────────────────────────────────────────────────────────────────────────────
28
  import streamlit as st
 
29
 
30
- # Pillow is required for the image‑to‑data‑URL monkey‑patch (for st_canvas).
31
  try:
32
- from PIL import Image # noqa: WPS433 – external import
33
  PIL_AVAILABLE = True
34
- except ImportError: # pragma: no cover – Space will show UI error banner
35
  PIL_AVAILABLE = False
36
- Image = None # type: ignore
 
 
 
 
37
 
38
- # ──────────────────────────────────────────────────────────────────────────────
39
- # Internal modules (each kept small & focused)
40
- # ──────────────────────────────────────────────────────────────────────────────
41
- from config import LOG_LEVEL, LOG_FORMAT, DATE_FORMAT, APP_CSS, FOOTER_MARKDOWN
 
 
 
 
 
 
 
 
 
 
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
- # Streamlit pageΒ config – **must** be first Streamlit call
50
- # -----------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  st.set_page_config(
52
- page_title="RadVisionΒ AI Advanced",
53
- page_icon="βš•οΈ",
54
  layout="wide",
55
  initial_sidebar_state="expanded",
56
  )
57
 
58
- # -----------------------------------------------------------------------------
59
- # Logging
60
- # -----------------------------------------------------------------------------
61
- for h in logging.root.handlers[:]:
62
- logging.root.removeHandler(h)
 
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("β†ͺ︎ RadVisionΒ AI bootstrap (StreamlitΒ v%s)", st.__version__)
72
 
73
- # -----------------------------------------------------------------------------
74
- # Session‑state defaults
75
- # -----------------------------------------------------------------------------
76
- initialize_session_state()
 
77
 
78
- # -----------------------------------------------------------------------------
79
- # Inject custom global CSS
80
- # -----------------------------------------------------------------------------
81
  st.markdown(APP_CSS, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- # -----------------------------------------------------------------------------
84
- # Monkey‑patch: st.elements.image.image_to_url β‡’ data‑URL encoder
85
- # Needed for streamlit‑drawable‑canvas screenshots
86
- # -----------------------------------------------------------------------------
87
- import streamlit.elements.image as _st_image # noqa: WPS433
88
-
89
- if not hasattr(_st_image, "image_to_url"):
90
-
91
- def _image_to_url_monkey_patch( # noqa: C901 – kept simple & explicit
92
- img_obj: Any,
93
- width: int = -1,
94
- clamp: bool = False,
95
- channels: str = "RGB",
96
- output_format: str = "auto",
97
- image_id: str = "",
98
- ) -> str:
99
- """Return a **data:URL** for a PILΒ Image so st_canvas can re‑render."""
100
- if not (PIL_AVAILABLE and isinstance(img_obj, Image.Image)):
101
- logger.warning("[patch] image_to_url: unsupported type %s", type(img_obj))
102
- return ""
103
-
104
- # ---------- normalise format ----------
105
- fmt = (output_format.upper() if output_format != "auto" else (img_obj.format or "PNG"))
106
- if fmt not in {"PNG", "JPEG", "WEBP", "GIF"}:
107
- fmt = "PNG"
108
- if img_obj.mode == "RGBA" and fmt == "JPEG": # JPEG has no alpha
109
- fmt = "PNG"
110
-
111
- # ---------- convert if needed ----------
112
- if img_obj.mode == "P": # palette β†’ RGBA
113
- img_obj = img_obj.convert("RGBA")
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
- # 3️⃣ Main page layout (left: viewerΒ Β· right: tabbed results)
136
- st.markdown("---")
137
- st.title("βš•οΈΒ RadVisionΒ AIΒ AdvancedΒ Β·Β AI‑AssistedΒ ImageΒ Analysis")
138
- with st.expander("User GuideΒ &Β Disclaimer", expanded=False):
139
- st.warning(
140
- "⚠️ **Disclaimer**: For research / educational use only – "
141
- "not intended for primary diagnostic decisions.",
142
- )
143
- st.markdown(
144
- """
145
- **Workflow**
146
- 1. **Upload** image (or enable **Demo Mode**).
147
- 2. *(DICOM)* adjust **Window / Level** if needed.
148
- 3. *(optional)* draw an **ROI** rectangle on the viewer.
149
- 4. Trigger **AI actions** from the sidebar.
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
- # Footer
165
- # -----------------------------------------------------------------------------
166
- st.markdown("---")
167
- st.caption(f"βš•οΈΒ RadVisionΒ AIΒ AdvancedΒ | SessionΒ ID:Β {st.session_state.get('session_id', 'N/A')}")
168
  st.markdown(FOOTER_MARKDOWN, unsafe_allow_html=True)
169
- logger.info("βœ“ Render cycle complete – SessionΒ IDΒ %s", st.session_state.get("session_id"))
 
 
 
 
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
+ # ---------------------------------------------------------------------------