| import gradio as gr |
| from transformers import pipeline |
|
|
| |
| ner = pipeline( |
| "token-classification", |
| model="d4data/biomedical-ner-all", |
| aggregation_strategy="simple", |
| ) |
|
|
| |
| COLOR_MAP = { |
| "Disease_disorder": "#FF6B6B", |
| "Sign_symptom": "#FFB347", |
| "Chemical": "#4ECDC4", |
| "Medication": "#4ECDC4", |
| "Body_part": "#A78BFA", |
| "Biological_structure":"#A78BFA", |
| "Diagnostic_procedure":"#60A5FA", |
| "Therapeutic_procedure":"#34D399", |
| "Lab_value": "#F9A8D4", |
| "Clinical_event": "#FCD34D", |
| "Date": "#94A3B8", |
| "Age": "#94A3B8", |
| "Severity": "#FB923C", |
| "Biological_attribute":"#C084FC", |
| } |
|
|
| EXAMPLES = [ |
| "Patient presents with acute onset of substernal chest pain radiating to the left arm. Started on aspirin and heparin drip.", |
| "The patient has a headache, fever, and sore throat. She was prescribed ibuprofen and amoxicillin.", |
| "The warrior stepped into the dungeon and drew his sword.", |
| ] |
|
|
| |
| def extract_entities(text: str): |
| if not text.strip(): |
| return [], [] |
|
|
| results = ner(text) |
|
|
| |
| highlighted = [] |
| cursor = 0 |
| for ent in results: |
| start, end = ent["start"], ent["end"] |
| label = ent.get("entity_group", ent.get("entity", "ENTITY")) |
| score = ent["score"] |
|
|
| |
| if start > cursor: |
| highlighted.append((text[cursor:start], None)) |
|
|
| highlighted.append((text[start:end], label)) |
| cursor = end |
|
|
| |
| if cursor < len(text): |
| highlighted.append((text[cursor:], None)) |
|
|
| |
| rows = [ |
| { |
| "Entity": ent["word"], |
| "Label": ent.get("entity_group", ent.get("entity", "ENTITY")), |
| "Score": round(float(ent["score"]), 2), |
| } |
| for ent in results |
| ] |
|
|
| return highlighted, rows |
|
|
|
|
| |
| css = """ |
| @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;500;700;800&family=IBM+Plex+Mono:wght@300;400;500&family=IBM+Plex+Sans:wght@300;400;500&display=swap'); |
| |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| :root { |
| --bg: #0D1117; |
| --surface: #161B22; |
| --border: #30363D; |
| --text: #E6EDF3; |
| --muted: #8B949E; |
| --accent: #58A6FF; |
| --accent2: #3FB950; |
| --danger: #FF7B72; |
| --mono: 'IBM Plex Mono', monospace; |
| --display: 'Syne', sans-serif; |
| --body: 'IBM Plex Sans', sans-serif; |
| --radius: 8px; |
| } |
| |
| body, .gradio-container { |
| background: var(--bg) !important; |
| font-family: var(--body) !important; |
| color: var(--text) !important; |
| } |
| |
| /* ── Header ── */ |
| .app-header { |
| padding: 32px 0 24px; |
| border-bottom: 1px solid var(--border); |
| margin-bottom: 28px; |
| display: flex; |
| align-items: flex-end; |
| justify-content: space-between; |
| flex-wrap: wrap; |
| gap: 12px; |
| } |
| .app-header h1 { |
| font-family: var(--display); |
| font-size: 2rem; |
| font-weight: 800; |
| letter-spacing: -0.03em; |
| color: var(--text); |
| line-height: 1; |
| } |
| .app-header h1 .accent { color: var(--accent); } |
| .header-right { |
| font-family: var(--mono); |
| font-size: 0.68rem; |
| letter-spacing: 0.1em; |
| text-transform: uppercase; |
| color: var(--muted); |
| text-align: right; |
| line-height: 1.8; |
| } |
| .tag { |
| display: inline-block; |
| background: #1F2937; |
| border: 1px solid var(--border); |
| color: var(--accent); |
| font-family: var(--mono); |
| font-size: 0.62rem; |
| letter-spacing: 0.08em; |
| padding: 2px 8px; |
| border-radius: 4px; |
| text-transform: uppercase; |
| } |
| |
| /* ── Section labels ── */ |
| .section-label { |
| font-family: var(--mono); |
| font-size: 0.65rem; |
| letter-spacing: 0.16em; |
| text-transform: uppercase; |
| color: var(--muted); |
| border-bottom: 1px solid var(--border); |
| padding-bottom: 8px; |
| margin-bottom: 12px; |
| } |
| |
| /* ── Inputs ── */ |
| textarea, input[type="text"] { |
| font-family: var(--mono) !important; |
| font-size: 0.84rem !important; |
| background: var(--surface) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: var(--radius) !important; |
| color: var(--text) !important; |
| line-height: 1.7 !important; |
| transition: border-color 0.15s; |
| } |
| textarea:focus, input:focus { |
| border-color: var(--accent) !important; |
| outline: none !important; |
| box-shadow: 0 0 0 3px rgba(88,166,255,0.15) !important; |
| } |
| |
| /* ── HighlightedText output ── */ |
| .highlighted-text-output { |
| background: var(--surface) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: var(--radius) !important; |
| padding: 16px !important; |
| font-family: var(--body) !important; |
| font-size: 0.92rem !important; |
| line-height: 1.9 !important; |
| color: var(--text) !important; |
| min-height: 80px; |
| } |
| /* entity chips */ |
| .highlighted-text-output mark, |
| .highlighted-text-output span[data-label] { |
| border-radius: 3px !important; |
| padding: 1px 4px !important; |
| font-weight: 500 !important; |
| } |
| |
| /* ── Dataframe ── */ |
| .dataframe-output table { |
| font-family: var(--mono) !important; |
| font-size: 0.78rem !important; |
| background: var(--surface) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: var(--radius) !important; |
| width: 100% !important; |
| border-collapse: collapse !important; |
| } |
| .dataframe-output th { |
| background: #1C2130 !important; |
| color: var(--accent) !important; |
| font-size: 0.65rem !important; |
| letter-spacing: 0.12em !important; |
| text-transform: uppercase !important; |
| padding: 10px 14px !important; |
| border-bottom: 1px solid var(--border) !important; |
| text-align: left !important; |
| } |
| .dataframe-output td { |
| color: var(--text) !important; |
| padding: 8px 14px !important; |
| border-bottom: 1px solid #1E242C !important; |
| } |
| .dataframe-output tr:hover td { background: #1A2030 !important; } |
| |
| /* ── Buttons ── */ |
| .analyze-btn { |
| background: var(--accent) !important; |
| color: #0D1117 !important; |
| font-family: var(--mono) !important; |
| font-size: 0.78rem !important; |
| font-weight: 500 !important; |
| letter-spacing: 0.1em !important; |
| text-transform: uppercase !important; |
| border: none !important; |
| border-radius: var(--radius) !important; |
| padding: 12px 28px !important; |
| cursor: pointer !important; |
| transition: opacity 0.15s, transform 0.1s !important; |
| } |
| .analyze-btn:hover { opacity: 0.85 !important; transform: translateY(-1px) !important; } |
| .analyze-btn:active { transform: translateY(0) !important; } |
| |
| .clear-btn { |
| background: transparent !important; |
| color: var(--muted) !important; |
| font-family: var(--mono) !important; |
| font-size: 0.75rem !important; |
| letter-spacing: 0.08em !important; |
| border: 1px solid var(--border) !important; |
| border-radius: var(--radius) !important; |
| padding: 10px 20px !important; |
| cursor: pointer !important; |
| transition: border-color 0.15s, color 0.15s !important; |
| } |
| .clear-btn:hover { |
| border-color: var(--danger) !important; |
| color: var(--danger) !important; |
| } |
| |
| /* ── Legend ── */ |
| .legend { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| margin: 16px 0 4px; |
| } |
| .legend-chip { |
| font-family: var(--mono); |
| font-size: 0.62rem; |
| letter-spacing: 0.06em; |
| padding: 3px 9px; |
| border-radius: 3px; |
| font-weight: 500; |
| color: #0D1117; |
| } |
| |
| /* ── Examples ── */ |
| .examples-section table { border-collapse: collapse; width: 100%; } |
| .examples-section td { |
| font-family: var(--mono) !important; |
| font-size: 0.75rem !important; |
| color: var(--muted) !important; |
| padding: 7px 12px !important; |
| border-bottom: 1px solid #1E242C !important; |
| cursor: pointer; |
| } |
| .examples-section tr:hover td { color: var(--text) !important; background: #1A2030 !important; } |
| |
| /* ── Disclaimer ── */ |
| .disclaimer { |
| margin-top: 32px; |
| padding: 12px 16px; |
| border: 1px solid var(--border); |
| border-left: 3px solid var(--muted); |
| border-radius: var(--radius); |
| background: var(--surface); |
| font-family: var(--mono); |
| font-size: 0.72rem; |
| color: var(--muted); |
| letter-spacing: 0.03em; |
| line-height: 1.6; |
| } |
| |
| /* ── Gradio chrome ── */ |
| .gradio-container > .main > .wrap { padding: 20px 32px 40px !important; } |
| footer { display: none !important; } |
| label span { font-family: var(--mono) !important; font-size: 0.7rem !important; |
| letter-spacing: 0.1em !important; text-transform: uppercase !important; |
| color: var(--muted) !important; } |
| """ |
|
|
| |
| with gr.Blocks(css=css, title="Medical Entity Extractor") as demo: |
|
|
| gr.HTML(""" |
| <div class="app-header"> |
| <div> |
| <div style="margin-bottom:8px;"> |
| <span class="tag">NER</span> |
| <span class="tag" style="margin-left:4px;">Biomedical</span> |
| <span class="tag" style="margin-left:4px;">d4data</span> |
| </div> |
| <h1>Medical <span class="accent">Entity</span> Extractor</h1> |
| </div> |
| <div class="header-right"> |
| d4data/biomedical-ner-all<br> |
| Diseases · Drugs · Symptoms · Body Parts · Procedures |
| </div> |
| </div> |
| |
| <div class="legend"> |
| <span class="legend-chip" style="background:#FF6B6B;">Disease / Disorder</span> |
| <span class="legend-chip" style="background:#FFB347;">Sign / Symptom</span> |
| <span class="legend-chip" style="background:#4ECDC4;">Chemical / Drug</span> |
| <span class="legend-chip" style="background:#A78BFA; color:#fff;">Body Part</span> |
| <span class="legend-chip" style="background:#60A5FA;">Diagnostic Procedure</span> |
| <span class="legend-chip" style="background:#34D399;">Therapeutic Procedure</span> |
| <span class="legend-chip" style="background:#F9A8D4;">Lab Value</span> |
| <span class="legend-chip" style="background:#FB923C;">Severity</span> |
| </div> |
| """) |
|
|
| with gr.Row(equal_height=False): |
| with gr.Column(scale=5): |
| gr.HTML('<div class="section-label">Input Text</div>') |
| text_input = gr.Textbox( |
| label="", |
| placeholder="Paste clinical notes, symptom descriptions, or any medical text…", |
| lines=8, |
| max_lines=20, |
| ) |
| with gr.Row(): |
| analyze_btn = gr.Button("Analyze →", elem_classes="analyze-btn") |
| clear_btn = gr.ClearButton( |
| components=[text_input], |
| value="Clear", |
| elem_classes="clear-btn", |
| ) |
|
|
| with gr.Column(scale=6): |
| gr.HTML('<div class="section-label">Highlighted Entities</div>') |
| highlighted_out = gr.HighlightedText( |
| label="", |
| color_map=COLOR_MAP, |
| show_legend=False, |
| elem_classes="highlighted-text-output", |
| ) |
|
|
| gr.HTML('<div class="section-label" style="margin-top:24px;">Entity Table</div>') |
| table_out = gr.Dataframe( |
| headers=["Entity", "Label", "Score"], |
| datatype=["str", "str", "number"], |
| label="", |
| elem_classes="dataframe-output", |
| wrap=True, |
| ) |
|
|
| gr.HTML('<div class="section-label" style="margin-top:24px;">Example Texts</div>') |
| gr.Examples( |
| examples=EXAMPLES, |
| inputs=text_input, |
| outputs=[highlighted_out, table_out], |
| fn=extract_entities, |
| cache_examples=False, |
| elem_id="examples-section", |
| ) |
|
|
| gr.HTML(""" |
| <div class="disclaimer"> |
| ℹ️ This model identifies biomedical terms in text. It does not provide medical advice. |
| Results are for research and educational purposes only. |
| </div> |
| """) |
|
|
| |
| analyze_btn.click( |
| fn=extract_entities, |
| inputs=text_input, |
| outputs=[highlighted_out, table_out], |
| ) |
| text_input.submit( |
| fn=extract_entities, |
| inputs=text_input, |
| outputs=[highlighted_out, table_out], |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |