Spaces:
Sleeping
Sleeping
| """ | |
| Module 1: Cross-Cultural Semantic Translator MVP | |
| ================================================= | |
| A medical AI platform for translating cultural pain metaphors into structured medical ontologies. | |
| """ | |
| import gradio as gr | |
| import json | |
| import os | |
| from typing import Dict, Tuple, Optional | |
| # ============================================================================ | |
| # CONFIGURATION | |
| # ============================================================================ | |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") | |
| TRANSCRIPTION_MODE = "api" | |
| OPENAI_MODEL = "gpt-5.2" | |
| # ============================================================================ | |
| # SETUP | |
| # ============================================================================ | |
| try: | |
| from openai import OpenAI | |
| client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None | |
| except ImportError: | |
| print("ERROR: OpenAI library not installed.") | |
| client = None | |
| # ============================================================================ | |
| # SYSTEM PROMPT | |
| # ============================================================================ | |
| MEDICAL_ANTHROPOLOGIST_PROMPT = """You are an expert Medical Anthropologist. Your goal is to translate cultural pain metaphors into structured medical ontologies. Do NOT act as a doctor making a final diagnosis. Analyze the patient's transcript and output a strict JSON object with these exact keys: 'literal_translation', 'metaphor_mapping', 'mcgill_pain_ontology', 'psychological_and_stoicism_flags', 'physician_action_note'. Make sure to include English and original language in metaphor_mapping for reference.""" | |
| # ============================================================================ | |
| # TRANSCRIPTION | |
| # ============================================================================ | |
| def transcribe_audio(audio_path: Optional[str]) -> Tuple[str, str]: | |
| if audio_path is None: | |
| return "", "โ ๏ธ No audio recorded." | |
| if client is None: | |
| return "", "โ OpenAI client not initialized." | |
| try: | |
| with open(audio_path, "rb") as audio_file: | |
| transcript = client.audio.transcriptions.create( | |
| model="whisper-1", | |
| file=audio_file, | |
| response_format="text" | |
| ) | |
| return transcript.strip(), "โ Transcribed via OpenAI Whisper API" | |
| except Exception as e: | |
| return "", f"โ Transcription error: {str(e)}" | |
| # ============================================================================ | |
| # LLM ANALYSIS | |
| # ============================================================================ | |
| def analyze_with_llm(transcription: str) -> Tuple[str, str]: | |
| if not transcription or not client: | |
| return "<div style='padding: 20px; color: #ff6b6b;'>โ Cannot analyze</div>", "{}" | |
| try: | |
| response = client.chat.completions.create( | |
| model=OPENAI_MODEL, | |
| messages=[ | |
| {"role": "system", "content": MEDICAL_ANTHROPOLOGIST_PROMPT}, | |
| {"role": "user", "content": f"Patient transcript:\n\n{transcription}"} | |
| ], | |
| response_format={"type": "json_object"}, | |
| temperature=0.7 | |
| ) | |
| json_text = response.choices[0].message.content | |
| parsed_json = json.loads(json_text) | |
| formatted_output = format_json_for_display(parsed_json) | |
| return formatted_output, json_text | |
| except Exception as e: | |
| import traceback | |
| error_html = f""" | |
| <div style='padding: 20px; background-color: #f8d7da; border-left: 5px solid #dc3545; border-radius: 8px;'> | |
| <h3 style='color: #721c24;'>โ Error</h3> | |
| <pre style='color: #721c24; font-size: 12px; overflow-x: auto;'>{traceback.format_exc()}</pre> | |
| </div> | |
| """ | |
| return error_html, "{}" | |
| # ============================================================================ | |
| # JSON FORMATTING - ๅฎๆด็ๆฌไป semantic_translator_mvp.py ๅคๅถ | |
| # ============================================================================ | |
| def format_json_for_display(data: Dict) -> str: | |
| """Format JSON into human-readable medical report""" | |
| html_parts = [''' | |
| <div style=" | |
| font-family: 'Segoe UI', Arial, sans-serif; | |
| padding: 30px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border-radius: 15px; | |
| color: #ffffff; | |
| box-shadow: 0 10px 25px rgba(0,0,0,0.2); | |
| line-height: 1.8; | |
| "> | |
| '''] | |
| # Debug section | |
| import json | |
| raw_json = json.dumps(data, indent=2, ensure_ascii=False) | |
| html_parts.append(f''' | |
| <details style="margin-bottom: 20px; padding: 15px; background-color: rgba(0, 0, 0, 0.2); border-radius: 8px;"> | |
| <summary style="cursor: pointer; font-weight: bold; color: #ffd700;">๐ Debug: Raw JSON</summary> | |
| <pre style="margin-top: 10px; padding: 10px; background-color: rgba(0, 0, 0, 0.3); border-radius: 5px; overflow-x: auto; font-size: 12px; color: #e0e0e0;">{raw_json}</pre> | |
| </details> | |
| ''') | |
| # 1. Literal Translation | |
| if 'literal_translation' in data: | |
| html_parts.append(f''' | |
| <div style="margin-bottom: 25px; padding: 20px; background-color: rgba(255,255,255,0.15); border-left: 5px solid #ffd700; border-radius: 10px;"> | |
| <h2 style="margin: 0 0 15px 0; color: #ffd700; font-size: 22px; font-weight: 700;">๐ Patient's Description</h2> | |
| <p style="margin: 0; font-size: 16px; color: #ffffff; font-style: italic;">"{data['literal_translation']}"</p> | |
| </div> | |
| ''') | |
| # 2. Metaphor Mapping | |
| if 'metaphor_mapping' in data: | |
| metaphor = data['metaphor_mapping'] | |
| html_parts.append(''' | |
| <div style="margin-bottom: 25px; padding: 20px; background-color: rgba(255,255,255,0.15); border-left: 5px solid #4fc3f7; border-radius: 10px;"> | |
| <h2 style="margin: 0 0 15px 0; color: #4fc3f7; font-size: 22px; font-weight: 700;">๐ Cultural Context</h2> | |
| ''') | |
| def render_value(val, indent=0): | |
| margin_left = indent * 20 | |
| if isinstance(val, dict): | |
| items = [] | |
| for k, v in val.items(): | |
| k_readable = k.replace('_', ' ').title() | |
| items.append(f'<div style="margin: 8px 0 8px {margin_left}px;"><strong style="color: #81d4fa;">{k_readable}:</strong>{render_value(v, indent+1)}</div>') | |
| return ''.join(items) | |
| elif isinstance(val, list): | |
| if not val: | |
| return '<span style="margin-left: 10px; color: #e0e0e0;">None</span>' | |
| items_html = '<ul style="margin: 5px 0; padding-left: 20px; color: #e0e0e0;">' | |
| for item in val: | |
| items_html += f'<li style="margin: 5px 0;">{render_value(item, indent) if isinstance(item, (dict, list)) else str(item)}</li>' | |
| items_html += '</ul>' | |
| return items_html | |
| else: | |
| return f'<span style="margin-left: 10px; font-size: 15px; color: #ffffff;">{str(val)}</span>' | |
| html_parts.append(render_value(metaphor)) | |
| html_parts.append('</div>') | |
| # 3. McGill Pain Ontology | |
| if 'mcgill_pain_ontology' in data: | |
| mcgill = data['mcgill_pain_ontology'] | |
| html_parts.append(''' | |
| <div style="margin-bottom: 25px; padding: 20px; background-color: rgba(255,255,255,0.15); border-left: 5px solid #ff6b6b; border-radius: 10px;"> | |
| <h2 style="margin: 0 0 15px 0; color: #ff6b6b; font-size: 22px; font-weight: 700;">๐ฅ McGill Pain Assessment</h2> | |
| ''') | |
| field_icons = { | |
| 'location': '๐', | |
| 'temporal_pattern': 'โฑ๏ธ', | |
| 'intensity': '๐', | |
| 'quality_descriptors': '๐ญ', | |
| 'associated_symptoms_to_query': '๐', | |
| 'functional_impact_to_query': '๐ถ', | |
| 'pain_or_sensory_type': '๐ฉบ' | |
| } | |
| def render_mcgill(val, indent=1): | |
| margin_left = indent * 20 | |
| if isinstance(val, dict): | |
| items = [] | |
| for k, v in val.items(): | |
| k_readable = k.replace('_', ' ').title() | |
| items.append(f'<div style="margin: 5px 0 5px {margin_left}px;"><em style="color: #ffd4d4;">{k_readable}:</em>{render_mcgill(v, indent+1)}</div>') | |
| return ''.join(items) | |
| elif isinstance(val, list): | |
| if not val: | |
| return '<span style="margin-left: 10px; color: #e0e0e0;">None specified</span>' | |
| return '<span style="margin-left: 10px; color: #ffffff;">' + ', '.join(str(v) for v in val) + '</span>' | |
| else: | |
| return f'<span style="margin-left: 10px; color: #ffffff;">{str(val)}</span>' | |
| if isinstance(mcgill, list): | |
| for item in mcgill: | |
| if isinstance(item, dict): | |
| for key, value in item.items(): | |
| key_readable = key.replace('_', ' ').title() | |
| icon = field_icons.get(key, 'โข') | |
| html_parts.append(f'<div style="margin-bottom: 15px; padding: 12px; background-color: rgba(255,255,255,0.1); border-radius: 8px;"><strong style="color: #ffcccb; font-size: 16px;">{icon} {key_readable}:</strong>{render_mcgill(value)}</div>') | |
| else: | |
| html_parts.append(f'<div style="margin-bottom: 15px; padding: 12px; background-color: rgba(255,255,255,0.1); border-radius: 8px;"><p style="margin: 0; font-size: 15px; color: #ffffff;">{str(item)}</p></div>') | |
| elif isinstance(mcgill, dict): | |
| for key, value in mcgill.items(): | |
| key_readable = key.replace('_', ' ').title() | |
| icon = field_icons.get(key, 'โข') | |
| html_parts.append(f'<div style="margin-bottom: 15px; padding: 12px; background-color: rgba(255,255,255,0.1); border-radius: 8px;"><strong style="color: #ffcccb; font-size: 16px;">{icon} {key_readable}:</strong>{render_mcgill(value)}</div>') | |
| else: | |
| html_parts.append(f'<div style="margin-bottom: 15px; padding: 12px; background-color: rgba(255,255,255,0.1); border-radius: 8px;"><p style="margin: 0; font-size: 15px; color: #ffffff;">{str(mcgill)}</p></div>') | |
| html_parts.append('</div>') | |
| # 4. Psychological Flags | |
| if 'psychological_and_stoicism_flags' in data: | |
| psych = data['psychological_and_stoicism_flags'] | |
| html_parts.append(''' | |
| <div style="margin-bottom: 25px; padding: 20px; background-color: rgba(255,255,255,0.15); border-left: 5px solid #9c27b0; border-radius: 10px;"> | |
| <h2 style="margin: 0 0 15px 0; color: #ce93d8; font-size: 22px; font-weight: 700;">๐ง Psychological Assessment</h2> | |
| ''') | |
| for key, value in psych.items(): | |
| key_readable = key.replace('_', ' ').title() | |
| if isinstance(value, dict): | |
| html_parts.append(f'<p style="margin: 10px 0; font-size: 15px;"><strong style="color: #ce93d8;">{key_readable}:</strong></p>') | |
| for sub_key, sub_value in value.items(): | |
| sub_key_readable = sub_key.replace('_', ' ').title() | |
| html_parts.append(f'<p style="margin: 5px 0 5px 20px; font-size: 14px; color: #e0e0e0;">โข {sub_key_readable}: {sub_value}</p>') | |
| else: | |
| html_parts.append(f'<p style="margin: 10px 0; font-size: 15px;"><strong style="color: #ce93d8;">{key_readable}:</strong> <span style="color: #ffffff;">{value}</span></p>') | |
| html_parts.append('</div>') | |
| # 5. Physician Action Note | |
| if 'physician_action_note' in data: | |
| html_parts.append(f''' | |
| <div style="padding: 20px; background-color: rgba(255,255,255,0.2); border: 3px solid #4caf50; border-radius: 10px;"> | |
| <h2 style="margin: 0 0 15px 0; color: #a5d6a7; font-size: 22px; font-weight: 700;">โ๏ธ Clinical Recommendations</h2> | |
| <p style="margin: 0; font-size: 16px; color: #ffffff; line-height: 1.9;">{data['physician_action_note']}</p> | |
| </div> | |
| ''') | |
| html_parts.append('</div>') | |
| return ''.join(html_parts) | |
| # ============================================================================ | |
| # MAIN PROCESSING | |
| # ============================================================================ | |
| def process_patient_audio(audio) -> Tuple[str, str, str]: | |
| try: | |
| transcription, trans_status = transcribe_audio(audio) | |
| if "Error" in trans_status or not transcription.strip(): | |
| return trans_status, transcription, "<div style='padding: 20px; color: #ff6b6b;'>โ ๏ธ Cannot analyze without transcription.</div>" | |
| formatted_html, json_output = analyze_with_llm(transcription) | |
| if "Error" in formatted_html: | |
| return "โ Analysis failed", transcription, formatted_html | |
| return "โ Analysis complete", transcription, formatted_html | |
| except Exception as e: | |
| import traceback | |
| error_html = f""" | |
| <div style='padding: 20px; background-color: #f8d7da; border-left: 5px solid #dc3545; border-radius: 8px;'> | |
| <h3 style='color: #721c24;'>โ Unexpected Error</h3> | |
| <pre style='color: #721c24; font-size: 12px; overflow-x: auto;'>{traceback.format_exc()}</pre> | |
| </div> | |
| """ | |
| return "โ Processing error", "Error during processing", error_html | |
| # ============================================================================ | |
| # GRADIO UI | |
| # ============================================================================ | |
| def create_ui(): | |
| with gr.Blocks(title="Medical AI Semantic Translator", theme=gr.themes.Soft()) as app: | |
| gr.Markdown(""" | |
| # ๐ฅ Module 1: Cross-Cultural Semantic Translator | |
| ### Translating Cultural Pain Metaphors into Medical Ontologies | |
| **Instructions:** Record your audio description, then click Analyze. | |
| """) | |
| status_output = gr.Textbox(label="Status", interactive=False, lines=1) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### ๐ค Audio Input") | |
| audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record Your Pain Description") | |
| submit_btn = gr.Button("๐ Analyze", variant="primary", size="lg") | |
| gr.Markdown("### ๐ Transcription") | |
| transcription_output = gr.Textbox(label="Whisper Transcription", interactive=False, lines=8) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### ๐ค AI Medical Anthropologist Analysis") | |
| analysis_output = gr.HTML(value='<div style="padding: 20px; text-align: center; color: #6c757d;">Analysis results will appear here...</div>') | |
| gr.Markdown(f""" | |
| --- | |
| **Configuration:** `API` mode | `{OPENAI_MODEL}` | |
| **Deployed on:** [Hugging Face Spaces](https://huggingface.co/spaces/DIrtyCha/Module1demo) | |
| """) | |
| submit_btn.click(fn=process_patient_audio, inputs=[audio_input], outputs=[status_output, transcription_output, analysis_output]) | |
| return app | |
| # ============================================================================ | |
| # MAIN | |
| # ============================================================================ | |
| if __name__ == "__main__": | |
| print("=" * 70) | |
| print("๐ Medical AI Semantic Translator MVP") | |
| print("=" * 70) | |
| if not OPENAI_API_KEY: | |
| print("โ ๏ธ WARNING: OPENAI_API_KEY not set!") | |
| print(" Go to Settings โ Repository Secrets") | |
| else: | |
| print("โ OpenAI API key loaded") | |
| print("=" * 70) | |
| app = create_ui() | |
| app.launch() |