sehat_scan_ai / app.py
adil9858's picture
Update app.py
fc08155 verified
import base64
import io
import gradio as gr
import tempfile
import os
from groq import Groq
from deepgram import DeepgramClient
# API Keys (replace with your actual keys)
groq_key = os.getenv("GROQ_API_KEY")
tts_key = os.getenv("DEEPGRAM_API_KEY")
# Initialize clients
groq_client = Groq(api_key=groq_key)
deepgram_client = DeepgramClient(api_key=tts_key)
def encode_image_to_data_url(image_file):
"""Convert uploaded image to base64 data URL"""
# Read image file
if hasattr(image_file, 'read'):
content = image_file.read()
else:
with open(image_file, "rb") as f:
content = f.read()
encoded = base64.b64encode(content).decode("utf-8")
# Determine MIME type based on filename
if isinstance(image_file, str):
filename = image_file.lower()
else:
filename = getattr(image_file, 'name', '').lower()
if filename.endswith('.png'):
return f"data:image/png;base64,{encoded}"
elif filename.endswith('.jpg') or filename.endswith('.jpeg'):
return f"data:image/jpeg;base64,{encoded}"
elif filename.endswith('.gif'):
return f"data:image/gif;base64,{encoded}"
elif filename.endswith('.bmp'):
return f"data:image/bmp;base64,{encoded}"
elif filename.endswith('.webp'):
return f"data:image/webp;base64,{encoded}"
else:
# Default to jpeg
return f"data:image/jpeg;base64,{encoded}"
def analyze_medical_report(image_data_url):
"""Analyze medical report using Groq API"""
completion = groq_client.chat.completions.create(
model="meta-llama/llama-4-scout-17b-16e-instruct",
messages=[
{
"role": "system",
"content": """You are a compassionate doctor explaining medical test results directly to your patient./
Speak to them using 'you' and 'your' in a warm, caring tone.
Your task is to explain the medical report accurately but in simple, everyday language that anyone can understand. /
When you see medical terms in the report, translate them into plain English immediately. For example, say "high blood sugar"/
instead of "hyperglycemia" or explain "lipid panel" as "cholesterol and fat levels in your blood."
Write your response as if you're having a gentle conversation with the patient. Connect all your thoughts /
smoothly so it flows naturally from one idea to the next. Start by warmly acknowledging them, then walk them/
through their results step by step, explaining what each finding means in practical terms. Help them understand/
the overall picture of their health and what these results suggest about their wellbeing. End with a supportive/
and encouraging note that leaves them feeling informed and cared for.
Remember: your voice should be consistently gentle, reassuring, and clear throughout the entire response./
Never break the conversational flow with abrupt transitions.
Never ask any counter questions whatsoever and never say ask me any questions ever and
never say something like this "If you have any concerns or questions, I'm here to support you"
ANSWER IN LESS THAN 500 words /characters
Structure your response:
1. Start with a warm greeting
2. Summarize the key findings
3. Explain each important result in simple terms
4. Provide context about what these results mean
5. End with a reassuring note"""
},
{
"role": "user",
"content": [
{"type": "text", "text": "I'd like you to review my medical report. Please explain it to me directly in simple, clear terms so I can understand it well:"},
{
"type": "image_url",
"image_url": {"url": image_data_url}
}
]
}
],
temperature=0.2,
max_completion_tokens=4096,
top_p=0.8,
stream=False,
)
return completion.choices[0].message.content
def text_to_speech(text):
"""Convert text to speech using Deepgram API"""
# Create temporary file
temp_file = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
temp_path = temp_file.name
response = deepgram_client.speak.v1.audio.generate(
text=text,
model="aura-asteria-en",
encoding="mp3"
)
# Save to temporary file
with open(temp_path, "wb") as f:
for chunk in response:
f.write(chunk)
return temp_path
def process_report(image):
"""Main processing function for Gradio"""
if image is None:
return None, None, "⚠️ Please upload a medical report image first."
try:
# Convert image to data URL
image_data_url = encode_image_to_data_url(image)
# Analyze with Groq
analysis = analyze_medical_report(image_data_url)
# Convert to speech
audio_path = text_to_speech(analysis)
return analysis, audio_path, "βœ… Analysis complete! Please review the results below."
except Exception as e:
error_msg = f"❌ An error occurred: {str(e)}\n\nPlease ensure:\n1. Your API keys are valid\n2. You have an active internet connection\n3. The uploaded file is a valid medical report image"
return None, None, error_msg
# Custom CSS for Dark Green Healthcare Theme
css = """
/* Main theme colors - Dark Theme with Green Accents */
:root {
--bg-primary: #0a1929;
--bg-secondary: #1a2536;
--bg-card: #15202b;
--text-primary: #ffffff;
--text-secondary: #b0b7c3;
--text-green: #4caf50;
--text-green-light: #81c784;
--text-green-dark: #388e3c;
--accent-green: #4caf50;
--accent-green-light: #66bb6a;
--accent-green-dark: #2e7d32;
--border-color: #2d3748;
--shadow-color: rgba(0, 0, 0, 0.3);
--success: #4caf50;
--warning: #ff9800;
--danger: #f44336;
--primary-gradient: linear-gradient(135deg, var(--accent-green-dark) 0%, var(--accent-green) 100%);
--hover-gradient: linear-gradient(135deg, var(--accent-green) 0%, var(--accent-green-light) 100%);
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-primary) !important;
color: var(--text-primary) !important;
min-height: 100vh;
}
/* Header styling */
.header-container {
background: linear-gradient(135deg, #0c2b4b 0%, var(--accent-green-dark) 100%);
padding: 2.5rem;
border-radius: 15px;
margin-bottom: 2rem;
color: white;
text-align: center;
border: 1px solid var(--border-color);
box-shadow: 0 8px 32px var(--shadow-color);
}
.header-title {
font-size: 2.8rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.header-subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 1rem;
color: var(--text-green-light);
}
/* Branding styling */
.branding {
background: rgba(76, 175, 80, 0.2);
padding: 0.75rem 1.5rem;
border-radius: 25px;
display: inline-block;
margin-top: 1rem;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-green-light);
border: 1px solid rgba(76, 175, 80, 0.3);
backdrop-filter: blur(10px);
}
/* Card styling */
.gradio-container {
max-width: 1200px !important;
margin: 0 auto !important;
padding: 20px !important;
background: var(--bg-primary) !important;
}
.gradio-card {
background: var(--bg-card) !important;
border-radius: 12px !important;
border: 1px solid var(--border-color) !important;
box-shadow: 0 4px 20px var(--shadow-color) !important;
padding: 1.5rem !important;
margin-bottom: 1.5rem !important;
backdrop-filter: blur(10px);
}
.card-title {
color: var(--text-green) !important;
font-weight: 600 !important;
font-size: 1.4rem !important;
margin-bottom: 1.2rem !important;
padding-bottom: 0.5rem !important;
border-bottom: 2px solid var(--accent-green) !important;
}
/* Button styling */
button.primary {
background: var(--primary-gradient) !important;
border: none !important;
color: white !important;
padding: 1rem 2rem !important;
border-radius: 10px !important;
font-weight: 600 !important;
font-size: 1.1rem !important;
transition: all 0.3s ease !important;
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3) !important;
}
button.primary:hover {
background: var(--hover-gradient) !important;
transform: translateY(-2px) !important;
box-shadow: 0 6px 25px rgba(76, 175, 80, 0.4) !important;
}
button.secondary {
background: rgba(76, 175, 80, 0.1) !important;
border: 2px solid var(--accent-green) !important;
color: var(--text-green) !important;
padding: 0.75rem 1.5rem !important;
border-radius: 8px !important;
font-weight: 600 !important;
transition: all 0.3s ease !important;
}
/* Output styling */
.analysis-output {
background: rgba(21, 32, 43, 0.8) !important;
border-left: 4px solid var(--accent-green) !important;
padding: 1.5rem !important;
border-radius: 10px !important;
font-size: 1.05rem !important;
line-height: 1.6 !important;
color: var(--text-green-light) !important;
border: 1px solid var(--border-color) !important;
}
.audio-player {
background: var(--bg-card) !important;
border-radius: 10px !important;
padding: 1rem !important;
margin-top: 1rem !important;
border: 1px solid var(--border-color) !important;
}
/* Status message */
.status-success {
background: linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(129, 199, 132, 0.1) 100%);
color: var(--text-green-light);
padding: 1rem;
border-radius: 8px;
border-left: 4px solid var(--success);
border: 1px solid rgba(76, 175, 80, 0.2);
}
.status-error {
background: linear-gradient(135deg, rgba(244, 67, 54, 0.1) 0%, rgba(239, 154, 154, 0.1) 100%);
color: #ff8a80;
padding: 1rem;
border-radius: 8px;
border-left: 4px solid var(--danger);
border: 1px solid rgba(244, 67, 54, 0.2);
}
.status-info {
background: linear-gradient(135deg, rgba(33, 150, 243, 0.1) 0%, rgba(100, 181, 246, 0.1) 100%);
color: #81d4fa;
padding: 1rem;
border-radius: 8px;
border-left: 4px solid #2196f3;
border: 1px solid rgba(33, 150, 243, 0.2);
}
/* Upload area styling */
.upload-area {
border: 2px dashed var(--accent-green) !important;
background: rgba(76, 175, 80, 0.05) !important;
border-radius: 10px !important;
padding: 2rem !important;
text-align: center !important;
transition: all 0.3s ease !important;
}
.upload-area:hover {
border-color: var(--accent-green-light) !important;
background: rgba(76, 175, 80, 0.1) !important;
box-shadow: 0 0 20px rgba(76, 175, 80, 0.2) !important;
}
/* Logo styling */
.logo-container {
text-align: center;
margin-bottom: 1.5rem;
}
.logo {
max-width: 200px;
height: auto;
margin: 0 auto;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
}
/* Footer styling */
.footer {
text-align: center;
margin-top: 2rem;
padding: 1.5rem;
color: var(--text-secondary);
font-size: 0.9rem;
border-top: 1px solid var(--border-color);
background: var(--bg-card);
border-radius: 10px;
border: 1px solid var(--border-color);
}
.footer p {
color: var(--text-green-light);
margin-bottom: 0.5rem;
}
/* Loading animation */
@keyframes pulse {
0%, 100% {
opacity: 1;
box-shadow: 0 0 20px rgba(76, 175, 80, 0.3);
}
50% {
opacity: 0.7;
box-shadow: 0 0 30px rgba(76, 175, 80, 0.6);
}
}
.loading {
animation: pulse 1.5s ease-in-out infinite;
}
/* Input styling */
input, textarea, select {
background: var(--bg-card) !important;
border: 1px solid var(--border-color) !important;
color: var(--text-green-light) !important;
border-radius: 8px !important;
}
input::placeholder, textarea::placeholder {
color: var(--text-secondary) !important;
}
/* Label styling */
label {
color: var(--text-green) !important;
font-weight: 500 !important;
margin-bottom: 0.5rem !important;
}
/* Progress bar */
progress {
background: var(--bg-card) !important;
}
progress::-webkit-progress-bar {
background: var(--bg-card) !important;
border-radius: 10px !important;
}
progress::-webkit-progress-value {
background: var(--primary-gradient) !important;
border-radius: 10px !important;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 10px;
background: var(--bg-card);
}
::-webkit-scrollbar-track {
background: var(--bg-card);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: var(--accent-green);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-green-light);
}
/* Tooltip */
.tooltip {
background: var(--bg-card) !important;
border: 1px solid var(--border-color) !important;
color: var(--text-green-light) !important;
}
/* Responsive design */
@media (max-width: 768px) {
.header-title {
font-size: 2rem;
}
.gradio-container {
padding: 10px !important;
}
.gradio-card {
padding: 1rem !important;
}
}
/* Grid styling */
.gr-row {
background: transparent !important;
}
.gr-column {
background: transparent !important;
}
/* Markdown styling */
.gr-markdown {
color: var(--text-green-light) !important;
}
.gr-markdown h1, .gr-markdown h2, .gr-markdown h3 {
color: var(--text-green) !important;
}
.gr-markdown a {
color: var(--accent-green-light) !important;
}
.gr-markdown code {
background: rgba(76, 175, 80, 0.1) !important;
color: var(--text-green-light) !important;
border: 1px solid rgba(76, 175, 80, 0.3) !important;
}
/* Additional glowing effects */
.glow {
animation: glow 2s ease-in-out infinite alternate;
}
@keyframes glow {
from {
box-shadow: 0 0 10px rgba(76, 175, 80, 0.2),
0 0 20px rgba(76, 175, 80, 0.1);
}
to {
box-shadow: 0 0 20px rgba(76, 175, 80, 0.4),
0 0 40px rgba(76, 175, 80, 0.2);
}
}
/* Custom copy button styling */
.copy-btn-container {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.copy-btn {
background: rgba(76, 175, 80, 0.2) !important;
border: 1px solid var(--accent-green) !important;
color: var(--text-green-light) !important;
padding: 5px 15px !important;
border-radius: 5px !important;
cursor: pointer !important;
font-size: 0.9rem !important;
}
.copy-btn:hover {
background: rgba(76, 175, 80, 0.3) !important;
}
"""
# HTML for header with logo
header_html = """
<div class="header-container">
<div class="logo-container">
<img src="https://i.imgur.com/logo_var1.png" alt="HealthAI Logo" class="logo glow" onerror="this.onerror=null; this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjEwMCIgY3k9IjEwMCIgcj0iODAiIGZpbGw9IiMwYTE5MjkiIHN0cm9rZT0iIzRjYWY1MCIgc3Ryb2tlLXdpZHRoPSI0Ii8+CjxwYXRoIGQ9Ik03NSA5MEwxMDUgOTBMMTA1IDE0MEg5NVYxMjBINzVWMTAwSDg1IiBzdHJva2U9IiM0Y2FmNTAiIHN0cm9rZS13aWR0aD0iNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xMjUgMTEwTDE0MCA5NUwxNTUgMTEwIiBzdHJva2U9IiM0Y2FmNTAiIHN0cm9rZS13aWR0aD0iNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPg==';">
</div>
<h1 class="header-title">🩺 HealthAI Report Analyzer</h1>
<p class="header-subtitle">Transforming complex medical reports into clear, understandable insights with AI-powered explanations</p>
<div class="branding glow">
πŸš€ Developed by Adil @ Koshur AI
</div>
</div>
"""
# HTML for footer
footer_html = """
<div class="footer">
<p>⚠️ <strong>Disclaimer:</strong> This tool provides AI-generated explanations and should not replace professional medical advice. Always consult with a healthcare provider for medical decisions.</p>
<p>πŸ”’ <strong>Privacy:</strong> Your data is processed securely and not stored on our servers.</p>
<p style="margin-top: 1rem; font-size: 0.8rem; color: #81c784;">Β© 2024 Koshur AI Health Technologies</p>
</div>
"""
# Create Gradio interface (removed theme and css from Blocks constructor)
with gr.Blocks() as demo:
gr.HTML(header_html)
with gr.Row():
with gr.Column(scale=1):
with gr.Group(elem_classes="gradio-card"):
gr.Markdown("### πŸ“€ Upload Medical Report", elem_classes="card-title")
image_input = gr.Image(
type="filepath",
label="Upload your medical report image",
elem_classes="upload-area",
interactive=True
)
analyze_btn = gr.Button(
"πŸ” Analyze Report",
variant="primary",
size="lg",
elem_classes="primary",
scale=1
)
with gr.Column(scale=2):
with gr.Group(elem_classes="gradio-card"):
gr.Markdown("### πŸ“‹ Analysis Results", elem_classes="card-title")
status_output = gr.Markdown(
"πŸ‘† Upload a medical report image and click 'Analyze Report' to begin.",
elem_classes="status-info"
)
# Removed show_copy_button parameter
text_output = gr.Textbox(
label="Doctor's Explanation",
lines=14,
max_lines=25,
elem_classes="analysis-output"
)
with gr.Row():
with gr.Column():
with gr.Group(elem_classes="gradio-card"):
gr.Markdown("### πŸ”Š Audio Explanation", elem_classes="card-title")
audio_output = gr.Audio(
label="Listen to the explanation",
type="filepath",
elem_classes="audio-player",
interactive=True
)
gr.Markdown(
"🎧 The audio provides a gentle, spoken explanation of your medical report. "
"You can download it for future reference.",
elem_classes="status-info"
)
gr.HTML(footer_html)
# Instructions section
with gr.Group(elem_classes="gradio-card"):
gr.Markdown("### πŸ’‘ How to Use", elem_classes="card-title")
gr.Markdown("""
<div style="color: #81c784;">
1. **Upload** a clear image of your medical report (JPG, PNG, PDF)<br>
2. **Click** the "Analyze Report" button<br>
3. **Read** the clear, simple explanation<br>
4. **Listen** to the audio version for a gentle walkthrough<br>
5. **Download** the audio for offline listening
</div>
""")
gr.Markdown("### πŸ“ Supported Formats", elem_classes="card-title")
gr.Markdown("""
<div style="color: #81c784;">
β€’ Blood test results<br>
β€’ Radiology reports<br>
β€’ Lab test reports<br>
β€’ Doctor's notes<br>
β€’ Scan images (X-Ray, MRI, CT)<br>
β€’ ECG/EKG reports
</div>
""")
# Set up the processing
analyze_btn.click(
fn=process_report,
inputs=[image_input],
outputs=[text_output, audio_output, status_output],
api_name="analyze"
)
# Launch the app with theme and css parameters
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
show_error=True,
debug=False,
css=css,
theme=gr.themes.Base(
primary_hue="green",
neutral_hue="slate",
font=gr.themes.GoogleFont("Segoe UI")
)
)