Module1demo / app.py
DIrtyCha's picture
model
ece3e37
"""
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()