Spaces:
Sleeping
Sleeping
| 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") | |
| ) | |
| ) |