godmodefounder commited on
Commit
b15df3c
·
0 Parent(s):

Initial commit: Prescription Explainer with MedGemma and FHIR export

Browse files
.gitignore ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ venv/
9
+ .venv
10
+ *.egg-info/
11
+ dist/
12
+ build/
13
+
14
+ # Environment variables
15
+ .env
16
+ *.env
17
+ !.env.example
18
+
19
+ # IDE
20
+ .vscode/
21
+ .idea/
22
+ *.swp
23
+ *.swo
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # Streamlit
30
+ .streamlit/secrets.toml
31
+
32
+ # Model cache
33
+ *.bin
34
+ *.safetensors
35
+ models/
36
+
37
+ # Logs
38
+ *.log
39
+
40
+ # Testing
41
+ .pytest_cache/
42
+ .coverage
43
+ htmlcov/
.streamlit/config.toml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor = "#4CAF50"
3
+ backgroundColor = "#FFFFFF"
4
+ secondaryBackgroundColor = "#F0F2F6"
5
+ textColor = "#262730"
6
+ font = "sans serif"
7
+
8
+ [server]
9
+ headless = true
10
+ enableCORS = false
11
+ enableXsrfProtection = true
README.md ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Prescription Explainer
3
+ emoji: 💊
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: streamlit
7
+ sdk_version: 1.40.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # 💊 Prescription Explainer
14
+
15
+ AI-powered prescription understanding for Southeast Asian patients using Google's MedGemma.
16
+
17
+ ## What it does
18
+
19
+ 1. **Upload** a prescription image (photo or scan)
20
+ 2. **Get** a clear, patient-friendly explanation
21
+ 3. **Translate** to your preferred language (Thai, Indonesian, Vietnamese, Cambodian, Hindi, Mandarin, English)
22
+ 4. **Export** FHIR-compliant health data for sharing with healthcare providers
23
+
24
+ ## Features
25
+
26
+ - **MedGemma-powered extraction** - Reads prescription images directly using Google's multimodal medical AI
27
+ - **Plain-language explanations** - No medical jargon, just clear instructions
28
+ - **Multilingual support** - 7 Southeast Asian languages
29
+ - **FHIR R4 export** - MedicationStatement and MedicationRequest resources for health data portability
30
+ - **Privacy-first** - No data stored, images processed and discarded
31
+
32
+ ## Technology Stack
33
+
34
+ - **AI Model**: MedGemma 1.5 4B (`google/medgemma-1.5-4b-it`)
35
+ - **Translation**: Gemma 3
36
+ - **Frontend**: Streamlit
37
+ - **FHIR**: fhir.resources (Python)
38
+ - **Deployment**: Hugging Face Spaces
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ pip install -r requirements.txt
44
+ streamlit run app.py
45
+ ```
46
+
47
+ ## Supported Languages
48
+
49
+ - English
50
+ - Thai (ไทย)
51
+ - Indonesian (Bahasa Indonesia)
52
+ - Vietnamese (Tiếng Việt)
53
+ - Cambodian/Khmer (ភាសាខ្មែរ)
54
+ - Hindi (हिन्दी)
55
+ - Mandarin Simplified (简体中文)
56
+
57
+ ## Disclaimer
58
+
59
+ This tool provides general information only. Always consult your healthcare provider for medical advice.
60
+
61
+ ---
62
+
63
+ Built for the Google AI Hackathon 2026 - MedGemma Track
app.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prescription Explainer - AI-powered prescription understanding for SE Asia."""
2
+
3
+ import logging
4
+
5
+ import streamlit as st
6
+ from PIL import Image
7
+
8
+ from src.constants import SUPPORTED_LANGUAGES
9
+ from src.fhir_generator import FhirGenerator, parse_medications_to_dict
10
+ from src.medgemma_service import MedGemmaService, load_medgemma_model
11
+ from src.translation_service import TranslationService, load_translation_model
12
+
13
+ # Configure logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Page configuration
18
+ st.set_page_config(
19
+ page_title="Prescription Explainer",
20
+ page_icon="💊",
21
+ layout="centered",
22
+ initial_sidebar_state="collapsed",
23
+ )
24
+
25
+
26
+ @st.cache_resource
27
+ def get_medgemma():
28
+ """Load and cache MedGemma model."""
29
+ with st.spinner("Loading AI model... This may take a moment on first run."):
30
+ model, processor = load_medgemma_model()
31
+ return MedGemmaService(model, processor)
32
+
33
+
34
+ @st.cache_resource
35
+ def get_translation_service():
36
+ """Load and cache translation model."""
37
+ with st.spinner("Loading translation model..."):
38
+ model, tokenizer = load_translation_model()
39
+ return TranslationService(model, tokenizer)
40
+
41
+
42
+ @st.cache_resource
43
+ def get_fhir_generator():
44
+ """Get FHIR generator instance."""
45
+ return FhirGenerator()
46
+
47
+
48
+ def main():
49
+ """Main application entry point."""
50
+ # Header
51
+ st.title("💊 Prescription Explainer")
52
+ st.markdown(
53
+ "Upload a prescription image to get a clear explanation in your language."
54
+ )
55
+
56
+ # Language selector
57
+ selected_language = st.selectbox(
58
+ "Choose your language / เลือกภาษา / Pilih bahasa",
59
+ options=list(SUPPORTED_LANGUAGES.keys()),
60
+ index=0,
61
+ )
62
+
63
+ # File uploader
64
+ uploaded_file = st.file_uploader(
65
+ "Upload prescription image",
66
+ type=["jpg", "jpeg", "png", "webp"],
67
+ help="Take a photo of your prescription or upload an existing image",
68
+ )
69
+
70
+ if uploaded_file is not None:
71
+ # Display uploaded image
72
+ image = Image.open(uploaded_file)
73
+ st.image(image, caption="Your prescription", use_container_width=True)
74
+
75
+ # Process button
76
+ if st.button("📋 Explain My Prescription", type="primary", use_container_width=True):
77
+ try:
78
+ # Load services
79
+ medgemma = get_medgemma()
80
+ translation_service = get_translation_service()
81
+ fhir_generator = get_fhir_generator()
82
+
83
+ # Step 1: Extract medications
84
+ with st.spinner("Reading your prescription..."):
85
+ extraction = medgemma.extract_medications(image)
86
+
87
+ # Step 2: Generate explanation using translation model
88
+ with st.spinner("Creating explanation..."):
89
+ from src.prompts import EXPLANATION_PROMPT
90
+ explanation_prompt = EXPLANATION_PROMPT.format(medication_info=extraction)
91
+ explanation = translation_service.generate_text(explanation_prompt)
92
+
93
+ # Step 3: Translate if needed
94
+ if selected_language != "English":
95
+ with st.spinner(f"Translating to {selected_language}..."):
96
+ explanation = translation_service.translate_text(
97
+ explanation, selected_language
98
+ )
99
+
100
+ # Display results
101
+ st.success("Done!")
102
+ st.markdown("---")
103
+ st.subheader("📖 Your Prescription Explained")
104
+ st.markdown(explanation)
105
+
106
+ # FHIR Export section
107
+ st.markdown("---")
108
+ st.subheader("📤 Export Health Data (FHIR)")
109
+
110
+ try:
111
+ medications = parse_medications_to_dict(extraction)
112
+
113
+ col1, col2 = st.columns(2)
114
+
115
+ with col1:
116
+ # Generate MedicationStatement for each medication
117
+ statements = []
118
+ for med in medications:
119
+ try:
120
+ statements.append(fhir_generator.generate_medication_statement(med))
121
+ except Exception as e:
122
+ logger.warning(f"Skipping FHIR generation for {med.get('drug_name', 'unknown')}: {e}")
123
+
124
+ if statements:
125
+ combined_statements = "[\n" + ",\n".join(statements) + "\n]"
126
+
127
+ st.download_button(
128
+ label="💾 MedicationStatement (JSON)",
129
+ data=combined_statements,
130
+ file_name="medication_statement.json",
131
+ mime="application/json",
132
+ )
133
+ else:
134
+ st.info("FHIR export not available for this prescription")
135
+
136
+ with col2:
137
+ # Generate MedicationRequest for each medication
138
+ requests = []
139
+ for med in medications:
140
+ try:
141
+ requests.append(fhir_generator.generate_medication_request(med))
142
+ except Exception as e:
143
+ logger.warning(f"Skipping FHIR generation for {med.get('drug_name', 'unknown')}: {e}")
144
+
145
+ if requests:
146
+ combined_requests = "[\n" + ",\n".join(requests) + "\n]"
147
+
148
+ st.download_button(
149
+ label="💾 MedicationRequest (JSON)",
150
+ data=combined_requests,
151
+ file_name="medication_request.json",
152
+ mime="application/json",
153
+ )
154
+ else:
155
+ st.info("FHIR export not available for this prescription")
156
+
157
+ st.caption(
158
+ "FHIR R4 compliant files for sharing with healthcare providers"
159
+ )
160
+ except Exception as e:
161
+ logger.error(f"FHIR export failed: {e}")
162
+ st.warning("FHIR export not available for this prescription")
163
+
164
+ except ValueError as e:
165
+ st.error(str(e))
166
+ except Exception as e:
167
+ logger.error(f"Processing failed: {e}")
168
+ st.error(
169
+ "Something went wrong. Please try again with a clearer image."
170
+ )
171
+
172
+ # Footer
173
+ st.markdown("---")
174
+ st.caption(
175
+ "⚠️ This tool provides general information only. "
176
+ "Always consult your healthcare provider for medical advice."
177
+ )
178
+ st.caption("Built with MedGemma for the Google AI Hackathon 2026")
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit==1.40.0
2
+ transformers>=4.50.0
3
+ torch
4
+ Pillow
5
+ fhir.resources
6
+ accelerate
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Prescription Explainer Services
src/constants.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Application constants for Prescription Explainer."""
2
+
3
+ # Model identifiers
4
+ MEDGEMMA_MODEL_ID = "google/medgemma-1.5-4b-it"
5
+ TRANSLATION_MODEL_ID = "google/gemma-3-4b-it" # Gemma 3 for translation
6
+
7
+ # Supported languages with display names and codes
8
+ SUPPORTED_LANGUAGES = {
9
+ "English": "en",
10
+ "Thai": "th",
11
+ "Indonesian": "id",
12
+ "Vietnamese": "vi",
13
+ "Cambodian (Khmer)": "km",
14
+ "Hindi": "hi",
15
+ "Mandarin (Simplified)": "zh",
16
+ }
17
+
18
+ # FHIR constants
19
+ FHIR_VERSION = "R4"
src/fhir_generator.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FHIR R4 resource generator for medication data."""
2
+
3
+ import json
4
+ import logging
5
+ from datetime import datetime
6
+ from typing import Any
7
+ from uuid import uuid4
8
+
9
+ from fhir.resources.medicationrequest import MedicationRequest
10
+ from fhir.resources.medicationstatement import MedicationStatement
11
+ from fhir.resources.codeableconcept import CodeableConcept
12
+ from fhir.resources.dosage import Dosage
13
+ from fhir.resources.reference import Reference
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class FhirGenerator:
19
+ """Generator for FHIR R4 medication resources."""
20
+
21
+ def generate_medication_statement(
22
+ self, medication_data: dict[str, Any]
23
+ ) -> str:
24
+ """
25
+ Generate a FHIR MedicationStatement resource.
26
+
27
+ Args:
28
+ medication_data: Dictionary with medication info
29
+ - drug_name: Name of the medication
30
+ - dosage: Dosage amount and unit
31
+ - frequency: How often to take
32
+ - route: Route of administration (optional)
33
+ - notes: Additional instructions (optional)
34
+
35
+ Returns:
36
+ JSON string of FHIR MedicationStatement
37
+ """
38
+ try:
39
+ # Ensure drug_name is non-empty
40
+ drug_name = medication_data.get("drug_name", "").strip()
41
+ if not drug_name:
42
+ drug_name = "Medication (see prescription)"
43
+
44
+ # Build dosage text
45
+ dosage_text = self._format_dosage_text(medication_data)
46
+ if not dosage_text or dosage_text == "As directed":
47
+ dosage_text = "See prescription for details"
48
+
49
+ # Build dosage with optional route
50
+ route_text = medication_data.get("route", "").strip()
51
+ dosage_kwargs = {"text": dosage_text}
52
+ if route_text:
53
+ dosage_kwargs["route"] = CodeableConcept(text=route_text)
54
+
55
+ statement = MedicationStatement(
56
+ id=str(uuid4()),
57
+ status="active",
58
+ medicationCodeableConcept=CodeableConcept(text=drug_name),
59
+ subject=Reference(reference="Patient/example"),
60
+ effectiveDateTime=datetime.now().isoformat(),
61
+ dosage=[Dosage(**dosage_kwargs)],
62
+ )
63
+
64
+ return statement.json(indent=2)
65
+
66
+ except Exception as e:
67
+ logger.error(f"Failed to generate MedicationStatement: {e}")
68
+ raise ValueError("Could not generate FHIR MedicationStatement.")
69
+
70
+ def generate_medication_request(
71
+ self, medication_data: dict[str, Any]
72
+ ) -> str:
73
+ """
74
+ Generate a FHIR MedicationRequest resource.
75
+
76
+ Args:
77
+ medication_data: Dictionary with medication info
78
+ - drug_name: Name of the medication
79
+ - dosage: Dosage amount and unit
80
+ - frequency: How often to take
81
+ - duration: How long to take
82
+ - route: Route of administration (optional)
83
+ - notes: Additional instructions (optional)
84
+
85
+ Returns:
86
+ JSON string of FHIR MedicationRequest
87
+ """
88
+ try:
89
+ # Ensure drug_name is non-empty
90
+ drug_name = medication_data.get("drug_name", "").strip()
91
+ if not drug_name:
92
+ drug_name = "Medication (see prescription)"
93
+
94
+ # Build dosage text
95
+ dosage_text = self._format_dosage_text(medication_data)
96
+ if not dosage_text or dosage_text == "As directed":
97
+ dosage_text = "See prescription for details"
98
+
99
+ # Build dosage with optional route
100
+ route_text = medication_data.get("route", "").strip()
101
+ dosage_kwargs = {"text": dosage_text}
102
+ if route_text:
103
+ dosage_kwargs["route"] = CodeableConcept(text=route_text)
104
+
105
+ request = MedicationRequest(
106
+ id=str(uuid4()),
107
+ status="active",
108
+ intent="order",
109
+ medicationCodeableConcept=CodeableConcept(text=drug_name),
110
+ subject=Reference(reference="Patient/example"),
111
+ authoredOn=datetime.now().isoformat(),
112
+ dosageInstruction=[Dosage(**dosage_kwargs)],
113
+ )
114
+
115
+ return request.json(indent=2)
116
+
117
+ except Exception as e:
118
+ logger.error(f"Failed to generate MedicationRequest: {e}")
119
+ raise ValueError("Could not generate FHIR MedicationRequest.")
120
+
121
+ def _format_dosage_text(self, medication_data: dict[str, Any]) -> str:
122
+ """Format dosage information as human-readable text."""
123
+ parts = []
124
+
125
+ if dosage := medication_data.get("dosage"):
126
+ parts.append(dosage)
127
+
128
+ if frequency := medication_data.get("frequency"):
129
+ parts.append(frequency)
130
+
131
+ if duration := medication_data.get("duration"):
132
+ parts.append(f"for {duration}")
133
+
134
+ if notes := medication_data.get("notes"):
135
+ parts.append(f"({notes})")
136
+
137
+ return " ".join(parts) if parts else "As directed"
138
+
139
+
140
+ def parse_medications_to_dict(extraction_text: str) -> list[dict[str, Any]]:
141
+ """
142
+ Parse extracted medication text into structured dictionaries.
143
+
144
+ Args:
145
+ extraction_text: Raw text from MedGemma extraction
146
+
147
+ Returns:
148
+ List of medication dictionaries
149
+ """
150
+ # Simple parsing - in production, would use more robust NLP
151
+ medications = []
152
+
153
+ lines = extraction_text.strip().split("\n")
154
+ current_med = {}
155
+
156
+ for line in lines:
157
+ line = line.strip()
158
+ if not line:
159
+ if current_med:
160
+ medications.append(current_med)
161
+ current_med = {}
162
+ continue
163
+
164
+ line_lower = line.lower()
165
+
166
+ if "drug" in line_lower or "medication" in line_lower or "name:" in line_lower:
167
+ if current_med:
168
+ medications.append(current_med)
169
+ current_med = {"drug_name": line.split(":", 1)[-1].strip()}
170
+ elif "dosage" in line_lower or "dose:" in line_lower:
171
+ current_med["dosage"] = line.split(":", 1)[-1].strip()
172
+ elif "frequency" in line_lower or "times" in line_lower:
173
+ current_med["frequency"] = line.split(":", 1)[-1].strip()
174
+ elif "duration" in line_lower or "days" in line_lower:
175
+ current_med["duration"] = line.split(":", 1)[-1].strip()
176
+ elif "route" in line_lower:
177
+ current_med["route"] = line.split(":", 1)[-1].strip()
178
+ elif "instruction" in line_lower or "note" in line_lower:
179
+ current_med["notes"] = line.split(":", 1)[-1].strip()
180
+
181
+ if current_med:
182
+ medications.append(current_med)
183
+
184
+ return medications if medications else [{"drug_name": "See prescription details", "notes": extraction_text}]
src/medgemma_service.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MedGemma service for prescription extraction and explanation."""
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+ import torch
7
+ from PIL import Image
8
+ from transformers import AutoModelForImageTextToText, AutoProcessor
9
+
10
+ from .constants import MEDGEMMA_MODEL_ID
11
+ from .prompts import EXTRACTION_PROMPT, EXPLANATION_PROMPT
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class MedGemmaService:
17
+ """Service for extracting medications from prescription images using MedGemma."""
18
+
19
+ def __init__(self, model, processor):
20
+ """Initialize with pre-loaded model and processor."""
21
+ self.model = model
22
+ self.processor = processor
23
+
24
+ def extract_medications(self, image: Image.Image) -> str:
25
+ """
26
+ Extract medication information from a prescription image.
27
+
28
+ Args:
29
+ image: PIL Image of the prescription
30
+
31
+ Returns:
32
+ Extracted medication information as text
33
+
34
+ Raises:
35
+ ValueError: If extraction fails
36
+ """
37
+ try:
38
+ messages = [
39
+ {
40
+ "role": "user",
41
+ "content": [
42
+ {"type": "image", "image": image},
43
+ {"type": "text", "text": EXTRACTION_PROMPT},
44
+ ],
45
+ }
46
+ ]
47
+
48
+ inputs = self.processor.apply_chat_template(
49
+ messages,
50
+ add_generation_prompt=True,
51
+ tokenize=True,
52
+ return_dict=True,
53
+ return_tensors="pt",
54
+ ).to(self.model.device)
55
+
56
+ input_len = inputs["input_ids"].shape[-1]
57
+
58
+ with torch.inference_mode():
59
+ outputs = self.model.generate(
60
+ **inputs,
61
+ max_new_tokens=1024,
62
+ do_sample=False,
63
+ )
64
+
65
+ response = self.processor.decode(
66
+ outputs[0][input_len:], skip_special_tokens=True
67
+ )
68
+
69
+ return response.strip()
70
+
71
+ except Exception as e:
72
+ logger.error(f"Medication extraction failed: {e}")
73
+ raise ValueError(
74
+ "Could not read the prescription. Please try uploading a clearer image."
75
+ )
76
+
77
+ def generate_explanation(self, medication_info: str) -> str:
78
+ """
79
+ Generate a plain-language explanation of medications.
80
+
81
+ Args:
82
+ medication_info: Extracted medication information
83
+
84
+ Returns:
85
+ Patient-friendly explanation
86
+
87
+ Raises:
88
+ ValueError: If explanation generation fails
89
+ """
90
+ try:
91
+ prompt = EXPLANATION_PROMPT.format(medication_info=medication_info)
92
+
93
+ messages = [{"role": "user", "content": prompt}]
94
+
95
+ inputs = self.processor.apply_chat_template(
96
+ messages,
97
+ add_generation_prompt=True,
98
+ tokenize=True,
99
+ return_dict=True,
100
+ return_tensors="pt",
101
+ ).to(self.model.device)
102
+
103
+ input_len = inputs["input_ids"].shape[-1]
104
+
105
+ with torch.inference_mode():
106
+ outputs = self.model.generate(
107
+ **inputs,
108
+ max_new_tokens=1024,
109
+ do_sample=False,
110
+ )
111
+
112
+ response = self.processor.decode(
113
+ outputs[0][input_len:], skip_special_tokens=True
114
+ )
115
+
116
+ return response.strip()
117
+
118
+ except Exception as e:
119
+ logger.error(f"Explanation generation failed: {e}", exc_info=True)
120
+ raise ValueError(
121
+ f"Could not generate explanation: {str(e)}"
122
+ )
123
+
124
+
125
+ def load_medgemma_model():
126
+ """
127
+ Load MedGemma model and processor.
128
+
129
+ Returns:
130
+ Tuple of (model, processor)
131
+ """
132
+ logger.info(f"Loading MedGemma model: {MEDGEMMA_MODEL_ID}")
133
+
134
+ processor = AutoProcessor.from_pretrained(MEDGEMMA_MODEL_ID)
135
+ model = AutoModelForImageTextToText.from_pretrained(
136
+ MEDGEMMA_MODEL_ID,
137
+ torch_dtype=torch.bfloat16,
138
+ device_map="auto",
139
+ )
140
+
141
+ logger.info("MedGemma model loaded successfully")
142
+ return model, processor
src/prompts.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Centralized prompt templates for Prescription Explainer."""
2
+
3
+ EXTRACTION_PROMPT = """You are a medical AI assistant. Analyze this prescription image and extract all medication information.
4
+
5
+ For each medication found, provide:
6
+ 1. Drug name (generic and brand if visible)
7
+ 2. Dosage (amount and unit)
8
+ 3. Frequency (how often to take)
9
+ 4. Duration (how long to take)
10
+ 5. Route (oral, topical, etc.)
11
+ 6. Special instructions (if any)
12
+
13
+ Format your response as a structured list. If you cannot read part of the prescription clearly, indicate that.
14
+
15
+ Extract the medications from this prescription image:"""
16
+
17
+ EXPLANATION_PROMPT = """You are a friendly healthcare assistant explaining medications to patients in simple terms.
18
+
19
+ Given this medication information:
20
+ {medication_info}
21
+
22
+ Provide a clear, easy-to-understand explanation that includes:
23
+ 1. What each medication is for (in simple terms)
24
+ 2. How to take it correctly
25
+ 3. Important things to remember
26
+ 4. Common side effects to watch for (if applicable)
27
+
28
+ Use simple language that anyone can understand. Avoid medical jargon.
29
+ Be encouraging and supportive in your tone."""
30
+
31
+ TRANSLATION_PROMPT = """Translate the following healthcare information to {target_language}.
32
+
33
+ Keep the translation:
34
+ - Accurate and faithful to the original meaning
35
+ - Easy to understand for patients
36
+ - Culturally appropriate
37
+
38
+ Text to translate:
39
+ {text}
40
+
41
+ Provide only the translation, no explanations."""
src/translation_service.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Translation service using Gemma 3 for multilingual support."""
2
+
3
+ import logging
4
+
5
+ import torch
6
+ from transformers import AutoModelForCausalLM, AutoTokenizer
7
+
8
+ from .constants import SUPPORTED_LANGUAGES, TRANSLATION_MODEL_ID
9
+ from .prompts import TRANSLATION_PROMPT
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class TranslationService:
15
+ """Service for translating text to supported languages using Gemma 3."""
16
+
17
+ def __init__(self, model, tokenizer):
18
+ """Initialize with pre-loaded model and tokenizer."""
19
+ self.model = model
20
+ self.tokenizer = tokenizer
21
+
22
+ def generate_text(self, prompt: str) -> str:
23
+ """
24
+ Generate text using Gemma 3 (for explanations).
25
+
26
+ Args:
27
+ prompt: Text prompt for generation
28
+
29
+ Returns:
30
+ Generated text
31
+
32
+ Raises:
33
+ ValueError: If generation fails
34
+ """
35
+ try:
36
+ messages = [{"role": "user", "content": prompt}]
37
+
38
+ inputs = self.tokenizer.apply_chat_template(
39
+ messages,
40
+ add_generation_prompt=True,
41
+ tokenize=True,
42
+ return_dict=True,
43
+ return_tensors="pt",
44
+ ).to(self.model.device)
45
+
46
+ input_len = inputs["input_ids"].shape[-1]
47
+
48
+ with torch.inference_mode():
49
+ outputs = self.model.generate(
50
+ **inputs,
51
+ max_new_tokens=2048,
52
+ do_sample=True,
53
+ temperature=0.7,
54
+ )
55
+
56
+ response = self.tokenizer.decode(
57
+ outputs[0][input_len:], skip_special_tokens=True
58
+ )
59
+
60
+ return response.strip()
61
+
62
+ except Exception as e:
63
+ logger.error(f"Text generation failed: {e}")
64
+ raise ValueError("Could not generate text. Please try again.")
65
+
66
+ def translate_text(self, text: str, target_language: str) -> str:
67
+ """
68
+ Translate text to the target language.
69
+
70
+ Args:
71
+ text: Text to translate (in English)
72
+ target_language: Target language display name (e.g., "Thai")
73
+
74
+ Returns:
75
+ Translated text
76
+
77
+ Raises:
78
+ ValueError: If translation fails or language not supported
79
+ """
80
+ if target_language not in SUPPORTED_LANGUAGES:
81
+ raise ValueError(f"Unsupported language: {target_language}")
82
+
83
+ # If target is English, no translation needed
84
+ if target_language == "English":
85
+ return text
86
+
87
+ try:
88
+ prompt = TRANSLATION_PROMPT.format(
89
+ target_language=target_language,
90
+ text=text,
91
+ )
92
+
93
+ return self.generate_text(prompt)
94
+
95
+ except Exception as e:
96
+ logger.error(f"Translation to {target_language} failed: {e}")
97
+ raise ValueError(
98
+ f"Could not translate to {target_language}. Please try again."
99
+ )
100
+
101
+
102
+ def load_translation_model():
103
+ """
104
+ Load Gemma 3 model for translation.
105
+
106
+ Returns:
107
+ Tuple of (model, tokenizer)
108
+ """
109
+ logger.info(f"Loading translation model: {TRANSLATION_MODEL_ID}")
110
+
111
+ tokenizer = AutoTokenizer.from_pretrained(TRANSLATION_MODEL_ID)
112
+ model = AutoModelForCausalLM.from_pretrained(
113
+ TRANSLATION_MODEL_ID,
114
+ torch_dtype=torch.bfloat16,
115
+ device_map="auto",
116
+ )
117
+
118
+ logger.info("Translation model loaded successfully")
119
+ return model, tokenizer