~/acute-dyspnea-ppt/build_ppt.py
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
from io import BytesIO
# ─── Colour Palette ───────────────────────────────────────────
DARK_BLUE = RGBColor(0x0A, 0x29, 0x5C) # navy header
MED_BLUE = RGBColor(0x1A, 0x5E, 0xA8) # accent
LIGHT_BLUE = RGBColor(0xD6, 0xE8, 0xF7) # light bg
TEAL = RGBColor(0x00, 0x7A, 0x87) # teal accent
GREEN = RGBColor(0x1B, 0x7A, 0x3D) # positive / safe
RED = RGBColor(0xC0, 0x39, 0x2B) # danger / urgent
ORANGE = RGBColor(0xD3, 0x6B, 0x0B) # warning
YELLOW_BG = RGBColor(0xFF, 0xF3, 0xCD) # highlight bg
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
DARK_GRAY = RGBColor(0x2D, 0x2D, 0x2D)
MID_GRAY = RGBColor(0x60, 0x60, 0x60)
LIGHT_GRAY = RGBColor(0xF2, 0xF2, 0xF2)
LIME = RGBColor(0x27, 0xAE, 0x60)
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
blank = prs.slide_layouts[6] # fully blank
# ─── Helper Functions ─────────────────────────────────────────
def add_rect(slide, x, y, w, h, fill_color, line_color=None, line_width=Pt(0), radius=False):
shape_type = MSO_SHAPE.ROUNDED_RECTANGLE if radius else MSO_SHAPE.RECTANGLE
shp = slide.shapes.add_shape(shape_type, Inches(x), Inches(y), Inches(w), Inches(h))
shp.fill.solid()
shp.fill.fore_color.rgb = fill_color
if line_color:
shp.line.color.rgb = line_color
shp.line.width = line_width
else:
shp.line.fill.background()
shp.shadow.inherit = False
return shp
def add_textbox(slide, x, y, w, h, text, font_size=18, bold=False, color=DARK_GRAY,
align=PP_ALIGN.LEFT, italic=False, word_wrap=True):
tb = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
tf = tb.text_frame
tf.word_wrap = word_wrap
tf.margin_left = Pt(4)
tf.margin_right = Pt(4)
tf.margin_top = Pt(2)
tf.margin_bottom = Pt(2)
p = tf.paragraphs[0]
p.alignment = align
r = p.add_run()
r.text = text
r.font.size = Pt(font_size)
r.font.bold = bold
r.font.italic = italic
r.font.color.rgb = color
r.font.name = "Calibri"
return tb
def add_shape_text(shp, text, font_size=14, bold=False, color=WHITE, align=PP_ALIGN.CENTER):
tf = shp.text_frame
tf.word_wrap = True
tf.vertical_anchor = MSO_ANCHOR.MIDDLE
tf.margin_left = Pt(4)
tf.margin_right = Pt(4)
tf.margin_top = Pt(2)
tf.margin_bottom = Pt(2)
p = tf.paragraphs[0]
p.alignment = align
r = p.add_run()
r.text = text
r.font.size = Pt(font_size)
r.font.bold = bold
r.font.color.rgb = color
r.font.name = "Calibri"
def add_connector(slide, x1, y1, x2, y2, color=DARK_GRAY, width=Pt(2)):
ln = slide.shapes.add_connector(
MSO_CONNECTOR.STRAIGHT,
Inches(x1), Inches(y1), Inches(x2), Inches(y2))
ln.line.color.rgb = color
ln.line.width = width
def slide_header(slide, title, subtitle=None):
# Top bar
shp = add_rect(slide, 0, 0, 13.333, 0.8, DARK_BLUE)
add_shape_text(shp, title, font_size=26, bold=True, color=WHITE, align=PP_ALIGN.LEFT)
shp.text_frame.paragraphs[0].runs[0].font.size = Pt(26)
# left margin inside bar
shp.text_frame.margin_left = Pt(20)
if subtitle:
add_textbox(slide, 0.2, 0.82, 13, 0.32, subtitle, font_size=11,
color=MED_BLUE, bold=False, italic=True)
def bottom_bar(slide, note="Tintinalli's EM | Harrison's 22E (2025) | GINA 2026 | AHA/ACC 2026"):
shp = add_rect(slide, 0, 7.2, 13.333, 0.3, DARK_BLUE)
add_shape_text(shp, note, font_size=9, color=WHITE, align=PP_ALIGN.CENTER)
# ═══════════════════════════════════════════════════════════════
# SLIDE 1 – Title
# ═══════════════════════════════════════════════════════════════
s1 = prs.slides.add_slide(blank)
add_rect(s1, 0, 0, 13.333, 7.5, DARK_BLUE)
# decorative accent bar
add_rect(s1, 0, 3.8, 13.333, 0.08, MED_BLUE)
add_rect(s1, 0, 3.88, 13.333, 0.04, TEAL)
add_textbox(s1, 0.6, 1.2, 12, 1.2,
"ACUTE DYSPNEA IN THE EMERGENCY ROOM",
font_size=38, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
add_textbox(s1, 0.6, 2.5, 12, 0.7,
"Diagnostic Algorithms | Assessment | Management",
font_size=20, color=LIGHT_BLUE, align=PP_ALIGN.CENTER, italic=True)
add_textbox(s1, 0.6, 4.1, 12, 0.55,
"Postgraduate Emergency Medicine Teaching Module",
font_size=16, color=WHITE, align=PP_ALIGN.CENTER)
add_textbox(s1, 0.6, 4.7, 12, 0.5,
"Based on: Tintinalli's EM | Harrison's Principles 22E (2025) | GINA 2026 | AHA/ACC 2026 PE Guidelines | SFMU/FICS 2026",
font_size=11, color=LIGHT_BLUE, align=PP_ALIGN.CENTER, italic=True)
add_textbox(s1, 0.6, 5.4, 12, 0.5,
"July 2026", font_size=13, color=LIGHT_BLUE, align=PP_ALIGN.CENTER)
# ═══════════════════════════════════════════════════════════════
# SLIDE 2 – Definition & Epidemiology
# ═══════════════════════════════════════════════════════════════
s2 = prs.slides.add_slide(blank)
add_rect(s2, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s2, "DEFINITION & EPIDEMIOLOGY", "Harrison's Principles of Internal Medicine 22E (2025) – Chapter 39")
bottom_bar(s2)
# Definition box
shp_def = add_rect(s2, 0.3, 1.05, 8.2, 2.1, WHITE, MED_BLUE, Pt(2), radius=True)
add_textbox(s2, 0.5, 1.1, 8.0, 0.45, "DEFINITION (ATS Consensus — Harrison's 22E)", 13, True, MED_BLUE)
add_textbox(s2, 0.5, 1.55, 7.9, 1.5,
'"A subjective experience of breathing discomfort that consists of qualitatively distinct sensations that vary in intensity. '
'The experience derives from interactions among multiple physiological, psychological, social, and environmental factors."',
12, False, DARK_GRAY, PP_ALIGN.LEFT, italic=True)
# Key distinction box
shp_dis = add_rect(s2, 0.3, 3.25, 8.2, 1.35, YELLOW_BG, ORANGE, Pt(2), radius=True)
add_textbox(s2, 0.5, 3.3, 8.0, 0.4, "KEY DISTINCTION", 12, True, ORANGE)
add_textbox(s2, 0.5, 3.7, 8.0, 0.85,
"Dyspnea = SYMPTOM (patient self-reported only)\n"
"Tachypnea / accessory muscle use = SIGNS (measured by clinicians)",
12, False, DARK_GRAY)
# Epidemiology stats
epi_items = [
("3-4M", "ED visits/year\nfor dyspnea"),
("85%", "Cardiac + pulmonary\ncauses combined"),
("37%", "Prevalence in\nadults ≥70 yrs"),
("1/3", "Have multi-\nfactorial dyspnea"),
]
colors_epi = [MED_BLUE, TEAL, ORANGE, RED]
for i, (num, label) in enumerate(epi_items):
x = 8.75 + i * 1.15
shp_n = add_rect(s2, x, 1.1, 1.05, 0.65, colors_epi[i], radius=True)
add_shape_text(shp_n, num, 20, True, WHITE)
add_textbox(s2, x, 1.78, 1.1, 0.6, label, 9, False, MID_GRAY, PP_ALIGN.CENTER)
add_textbox(s2, 8.7, 1.0, 4.4, 0.35, "EPIDEMIOLOGY", 11, True, DARK_BLUE)
# Post-COVID note
shp_c = add_rect(s2, 8.7, 3.0, 4.4, 1.6, WHITE, MED_BLUE, Pt(1.5), radius=True)
add_textbox(s2, 8.8, 3.05, 4.2, 0.4, "NEW — Post-COVID Dyspnea", 11, True, MED_BLUE)
add_textbox(s2, 8.8, 3.45, 4.2, 1.0,
"Increasing recognition of persistent dyspnea in post-COVID syndrome (Ch. 205, Harrison's 22E). Often multifactorial.",
10, False, DARK_GRAY)
# ═══════════════════════════════════════════════════════════════
# SLIDE 3 – Mechanisms
# ═══════════════════════════════════════════════════════════════
s3 = prs.slides.add_slide(blank)
add_rect(s3, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s3, "MECHANISMS OF DYSPNEA", "Harrison's 22E — Chapter 39, Fig. 39-1")
bottom_bar(s3)
mech_blocks = [
(0.3, 1.15, 3.8, 4.9, LIGHT_BLUE, MED_BLUE, "AFFERENT SIGNALS → CNS",
"• Peripheral chemoreceptors (carotid body, aortic arch)\n"
" → Hypoxemia, Hypercapnia, Acidemia\n"
" → 'Air hunger' sensation\n\n"
"• Central chemoreceptors (medulla)\n"
" → Same triggers as above\n\n"
"• Mechanoreceptors (lungs, chest wall)\n"
" → Stretch receptors, irritant receptors, J receptors\n"
" → 'Chest tightness' (asthma/COPD)\n"
" → Muscle spindles / tendon organs"),
(4.4, 1.15, 3.8, 4.9, WHITE, TEAL, "EFFERENT SIGNALS: CNS → Muscles",
"• Increased motor output from CNS\n"
" to overcome obstruction / stiffness\n\n"
"• Occurs when airway resistance ↑\n"
" or lung/chest wall compliance ↓\n\n"
"• The brain commands muscles to\n"
" work harder\n\n"
"• Perceived as increased work of\n breathing"),
(8.5, 1.15, 4.5, 4.9, WHITE, RED, "EFFERENT-REAFFERENT MISMATCH",
"= Most intense dyspnea\n\n"
"When CNS motor commands do NOT\n"
"match the mechanical response:\n\n"
"• Neuromuscular disease\n"
"• Severe airway obstruction\n"
"• Dynamic hyperinflation\n"
"• Diaphragmatic weakness\n\n"
"Also called:\n"
"'Neuromechanical dissociation'\n\n"
"★ High-yield Harrison's 22E viva topic"),
]
for bx, by, bw, bh, bg, bc, title_text, body_text in mech_blocks:
shp = add_rect(s3, bx, by, bw, bh, bg, bc, Pt(2), radius=True)
add_textbox(s3, bx+0.1, by+0.05, bw-0.2, 0.4, title_text, 12, True, bc)
add_textbox(s3, bx+0.1, by+0.5, bw-0.2, bh-0.55, body_text, 11, False, DARK_GRAY)
# Arrow between boxes
add_connector(s3, 4.2, 3.6, 4.4, 3.6, MED_BLUE, Pt(2.5))
add_connector(s3, 8.3, 3.6, 8.5, 3.6, TEAL, Pt(2.5))
add_textbox(s3, 0.3, 6.15, 13.0, 0.4,
"Key Exam Point: Efferent-reafferent mismatch = single most important mechanism for most severe dyspnea (Harrison's 22E)",
11, True, RED, PP_ALIGN.CENTER)
# ═══════════════════════════════════════════════════════════════
# SLIDE 4 – Qualitative Descriptors
# ═══════════════════════════════════════════════════════════════
s4 = prs.slides.add_slide(blank)
add_rect(s4, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s4, "QUALITATIVE DESCRIPTORS → DIAGNOSTIC CLUES", "Harrison's 22E — History-taking in dyspnea")
bottom_bar(s4)
descriptors = [
("Chest tightness", "Asthma, COPD, Myocardial ischemia"),
("Air hunger / cannot\nget enough air", "CHF, Pulmonary edema, Severe airflow obstruction"),
("Cannot take a\ndeep breath", "Dynamic hyperinflation (COPD), Pleural effusion, Anxiety"),
("Orthopnea\n(worse lying flat)", "CHF, Diaphragmatic weakness, Obesity, GERD-asthma"),
("Paroxysmal nocturnal\ndyspnea (PND)", "CHF, Asthma (nocturnal trigger)"),
("Platypnea\n(worse UPRIGHT)", "Hepatopulmonary syndrome, Left atrial myxoma, ASD"),
("Acute episodic\ndyspnea", "PE, Myocardial ischemia, Bronchospasm, Arrhythmia"),
("Progressive exertional\ndyspnea", "COPD, ILD, CHF, Pulmonary hypertension"),
("Trepopnea\n(lateral position)", "Pleural effusion, Unilateral lung disease"),
]
cols = 3
rows_per_col = 3
for idx, (desc, cause) in enumerate(descriptors):
col = idx % cols
row = idx // cols
x = 0.25 + col * 4.37
y = 1.1 + row * 2.0
shp_box = add_rect(s4, x, y, 4.2, 1.75, WHITE, MED_BLUE, Pt(1.5), radius=True)
add_textbox(s4, x+0.1, y+0.05, 4.0, 0.55, desc, 12, True, MED_BLUE)
add_textbox(s4, x+0.1, y+0.6, 4.0, 1.05, cause, 11, False, DARK_GRAY)
add_textbox(s4, 0.3, 7.0, 13.0, 0.18,
"★ EXAM CLASSIC: Platypnea (dyspnea WORSE sitting up) → Hepatopulmonary syndrome / Left atrial myxoma / ASD",
10, True, RED, PP_ALIGN.CENTER)
# ═══════════════════════════════════════════════════════════════
# SLIDE 5 – Differential Diagnosis
# ═══════════════════════════════════════════════════════════════
s5 = prs.slides.add_slide(blank)
add_rect(s5, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s5, "DIFFERENTIAL DIAGNOSIS — ACUTE DYSPNEA IN ED", "Harrison's 22E Table 34-3 | Tintinalli's EM")
bottom_bar(s5)
# "CARDS" framework header
shp_cards = add_rect(s5, 0.25, 0.9, 12.8, 0.5, MED_BLUE, radius=True)
add_shape_text(shp_cards, "LIFE-THREATENING CAUSES — 'CARDS' Framework (Must Not Miss)", 14, True, WHITE)
cards_items = [
("C – Cardiac", "Acute pulmonary edema (ADHF)\nACS / STEMI\nCardiac tamponade\nMalignant arrhythmia", RED),
("A – Airway", "Tension pneumothorax\nSevere asthma / Status asthmaticus\nUpper airway obstruction\nAnaphylaxis", ORANGE),
("R – Respiratory", "Massive PE\nARDS\nSevere pneumonia / Sepsis", MED_BLUE),
("D – Distributive", "Septic shock\nAnaphylactic shock\nNeurogenicc shock", TEAL),
("S – Structural", "Aortic dissection + hemothorax\nFlail chest\nMassive pleural effusion", GREEN),
]
for i, (title_c, body_c, col) in enumerate(cards_items):
x = 0.25 + i * 2.57
shp_c = add_rect(s5, x, 1.5, 2.45, 3.2, WHITE, col, Pt(2), radius=True)
add_textbox(s5, x+0.1, 1.55, 2.3, 0.45, title_c, 12, True, col)
add_textbox(s5, x+0.1, 2.0, 2.3, 2.6, body_c, 10.5, False, DARK_GRAY)
# Additional causes
add_textbox(s5, 0.25, 4.82, 13.0, 0.35, "ADDITIONAL CAUSES (Harrison's 22E)", 12, True, DARK_BLUE)
additional = [
"Metabolic/Toxic: DKA, Metabolic acidosis, CO poisoning, Salicylate toxicity, Severe anemia",
"Neuromuscular: Guillain-Barré, Myasthenic crisis, Diaphragm paralysis",
"Other: COPD exacerbation, Pleural effusion (large), ILD exacerbation, Anxiety/Panic (diagnosis of exclusion)",
]
for i, line in enumerate(additional):
y = 5.2 + i * 0.48
bg = LIGHT_BLUE if i % 2 == 0 else WHITE
shp_add = add_rect(s5, 0.25, y, 12.8, 0.42, bg, radius=False)
add_textbox(s5, 0.4, y+0.03, 12.5, 0.38, line, 10.5, False, DARK_GRAY)
# ═══════════════════════════════════════════════════════════════
# SLIDE 6 – Initial Assessment (ABCDE + Vitals)
# ═══════════════════════════════════════════════════════════════
s6 = prs.slides.add_slide(blank)
add_rect(s6, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s6, "INITIAL ASSESSMENT IN THE ED", "SFMU/FICS Guidelines 2026 (Ann Intensive Care) | Tintinalli's EM")
bottom_bar(s6)
# ABCDE
abcde = [
("A – AIRWAY", "Stridor (inspiratory=supraglottic; expiratory=infraglottic)\nTripod/sniffing position → epiglottitis\nAble to speak? (sentences/phrases/words/silent)"),
("B – BREATHING", "RR ≥30 = severe | Measure ≥30 sec (SFMU/FICS 2026)\nSymmetric expansion? | Silent chest = life-threatening\nAccessory muscles | Tracheal deviation (tension PTX)"),
("C – CIRCULATION", "Pulsus paradoxus: >10 mmHg = significant; >25 = severe\nJVP raised: ADHF / tamponade / massive PE / tension PTX\nS3 gallop = elevated LVEDP (ADHF)"),
("D – DISABILITY", "GCS / AVPU: altered consciousness + dyspnea = EMERGENCY\nAxiety/agitation: severe distress, consider hypoxia"),
("E – EXPOSURE", "Leg swelling (DVT-PE, CHF) | Rashes (anaphylaxis)\nAbdomen: ascites, hepatomegaly | Peripheral oedema"),
]
for i, (ltr, body) in enumerate(abcde):
x = 0.25 if i < 3 else (0.25 + 4.4 * (i - 3))
y = 1.05 + (i % 3) * 2.05 if i < 3 else 1.05
if i >= 3:
y = 1.05
x = 0.25 + 4.4 * (i - 3)
shp_a = add_rect(s6, x, y, 4.2, 1.9, WHITE, MED_BLUE, Pt(2), radius=True)
add_textbox(s6, x+0.1, y+0.05, 4.0, 0.42, ltr, 13, True, MED_BLUE)
add_textbox(s6, x+0.1, y+0.5, 4.0, 1.3, body, 10, False, DARK_GRAY)
# Auscultation table on right
auscult_data = [
("Bilateral fine crackles (basal)", "Pulmonary oedema / ADHF", LIGHT_BLUE),
("Bilateral coarse crackles", "Pneumonia, ARDS", LIGHT_BLUE),
("Diffuse expiratory wheeze", "Asthma, COPD, Cardiac asthma", WHITE),
("Unilateral wheeze", "Foreign body, Tumour", WHITE),
("Absent breath sounds", "Pneumothorax, Effusion", LIGHT_BLUE),
("Pleural rub", "Pleuritis, PE with infarct", LIGHT_BLUE),
]
add_textbox(s6, 8.8, 1.05, 4.3, 0.38, "AUSCULTATION GUIDE", 12, True, DARK_BLUE)
for i, (find, interp, bg) in enumerate(auscult_data):
y = 1.45 + i * 0.6
shp_au = add_rect(s6, 8.8, y, 4.3, 0.55, bg, MID_GRAY, Pt(0.5))
add_textbox(s6, 8.85, y+0.03, 2.05, 0.48, find, 9, True, DARK_GRAY)
add_textbox(s6, 10.93, y+0.03, 2.1, 0.48, interp, 9, False, MID_GRAY)
add_textbox(s6, 0.25, 7.0, 13.0, 0.18,
"SFMU/FICS 2026: RR must be measured for ≥30 seconds. Continuous vital sign monitoring reduces morbidity/mortality.",
10, True, RED, PP_ALIGN.CENTER)
# ═══════════════════════════════════════════════════════════════
# SLIDE 7 – Investigations
# ═══════════════════════════════════════════════════════════════
s7 = prs.slides.add_slide(blank)
add_rect(s7, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s7, "INVESTIGATIONS — STEPWISE APPROACH", "Tintinalli's EM | Harrison's 22E | SFMU/FICS 2026")
bottom_bar(s7)
# Immediate
shp_imm = add_rect(s7, 0.25, 0.9, 6.1, 2.65, WHITE, RED, Pt(2), radius=True)
add_textbox(s7, 0.35, 0.95, 5.9, 0.4, "IMMEDIATE — ALL ACUTE DYSPNEA", 12, True, RED)
imm_text = ("1. SpO2 — continuous pulse oximetry (mandatory)\n"
"2. 12-lead ECG — ACS, arrhythmia, RV strain (PE), LBBB (CHF)\n"
"3. CXR (portable) — effusion, cardiomegaly, PTX, consolidation, pulmonary oedema\n"
"4. ABG / VBG — oxygenation, ventilation, acid-base\n"
" • VBG: PaCO2 <40 mmHg reliably EXCLUDES hypercapnia\n"
" • ABG: if SpO2 <92% or concern for CO2 retention")
add_textbox(s7, 0.35, 1.38, 5.9, 2.1, imm_text, 10.5, False, DARK_GRAY)
# Biomarkers
shp_bio = add_rect(s7, 6.55, 0.9, 6.55, 2.65, WHITE, MED_BLUE, Pt(2), radius=True)
add_textbox(s7, 6.65, 0.95, 6.3, 0.4, "BIOMARKERS", 12, True, MED_BLUE)
bio_text = ("BNP <100 pg/mL → NPV 90% for CHF (rules out)\n"
"BNP >500 pg/mL → strongly supports CHF\n"
"NT-proBNP <300 → rules out CHF\n"
"NT-proBNP (age-stratified cutoffs: >900 if <50y, >1800 if 50-75y)\n"
"Troponin (hs-cTnI/T) → ACS, myocarditis, RV strain in PE\n"
"D-dimer → PE rule-out (use age-adjusted: age × 10 µg/L if >50y)\n"
"Lactate → Sepsis (>2 mmol/L elevated; >4 = septic shock)\n"
"★ CRP/PCT: INSUFFICIENT evidence to guide antibiotics in\n"
" acute dyspnea + suspected LRTI (SFMU/FICS 2026)")
add_textbox(s7, 6.65, 1.38, 6.3, 2.1, bio_text, 10.0, False, DARK_GRAY)
# POCUS BLUE Protocol
shp_pocus = add_rect(s7, 0.25, 3.65, 8.0, 3.25, WHITE, TEAL, Pt(2), radius=True)
add_textbox(s7, 0.35, 3.7, 7.8, 0.45, "POCUS — BLUE PROTOCOL (Bedside Lung Ultrasound in Emergency)", 12, True, TEAL)
blue_table = [
("A-lines (horizontal)", "Normal lung — dry lung", "Asthma / COPD", WHITE),
("B-lines ≥3/zone, bilateral", "Interstitial syndrome — WET lung", "ADHF / Pulmonary oedema", LIGHT_BLUE),
("Absent lung sliding", "Pneumothorax suspected", "Confirm with Lung Point", WHITE),
("Lung point (pathognomonic)", "Definitive pneumothorax sign", "Pneumothorax", LIGHT_BLUE),
("Consolidation (hepatization)", "Lung consolidation", "Pneumonia / Atelectasis", WHITE),
("Anechoic above diaphragm", "Pleural fluid", "Effusion / Haemothorax", LIGHT_BLUE),
]
for i, (find_b, meaning_b, dx_b, bg_b) in enumerate(blue_table):
y = 4.2 + i * 0.42
shp_bl = add_rect(s7, 0.3, y, 7.85, 0.38, bg_b, MID_GRAY, Pt(0.3))
add_textbox(s7, 0.35, y+0.02, 2.5, 0.33, find_b, 9, True, DARK_GRAY)
add_textbox(s7, 2.88, y+0.02, 2.7, 0.33, meaning_b, 9, False, MID_GRAY)
add_textbox(s7, 5.6, y+0.02, 2.5, 0.33, dx_b, 9, True, TEAL)
# Evidence column
shp_ev = add_rect(s7, 8.55, 3.65, 4.55, 3.25, WHITE, ORANGE, Pt(2), radius=True)
add_textbox(s7, 8.65, 3.7, 4.3, 0.4, "RECENT EVIDENCE", 12, True, ORANGE)
ev_text = ("Taheri et al., Eur J Emerg Med 2025\n"
"POCUS in prehospital non-trauma dyspnea\n"
"→ improves diagnostic accuracy (Meta-analysis)\n\n"
"Russell et al., Am J Emerg Med 2024\n"
"Prehospital lung US for ADHF\n"
"→ High diagnostic accuracy (Systematic review)\n\n"
"Jeffers et al., J Emerg Med 2025\n"
"POCUS for pulmonary oedema\n"
"→ High sensitivity & specificity (SR)")
add_textbox(s7, 8.65, 4.15, 4.3, 2.6, ev_text, 9.5, False, DARK_GRAY)
# ═══════════════════════════════════════════════════════════════
# SLIDE 8 – Clinical Decision Tools (PERC + Wells + BNP)
# ═══════════════════════════════════════════════════════════════
s8 = prs.slides.add_slide(blank)
add_rect(s8, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s8, "CLINICAL DECISION TOOLS", "PERC Rule | Wells Score | BNP/NT-proBNP | Framingham")
bottom_bar(s8)
# PERC
shp_perc = add_rect(s8, 0.25, 0.9, 4.2, 5.1, WHITE, RED, Pt(2), radius=True)
add_textbox(s8, 0.35, 0.95, 4.0, 0.42, "PERC RULE — PE Rule-Out", 12, True, RED)
add_textbox(s8, 0.35, 1.37, 4.0, 0.32, "(All 8 absent in LOW pre-test probability = exclude PE)", 9, True, MID_GRAY)
perc_criteria = [
"1. Age < 50 years",
"2. HR < 100 bpm",
"3. SpO2 ≥ 95%",
"4. No unilateral leg swelling",
"5. No haemoptysis",
"6. No recent surgery/trauma",
"7. No prior PE/DVT",
"8. No exogenous oestrogen",
]
for i, crit in enumerate(perc_criteria):
y = 1.75 + i * 0.47
bg_p = LIGHT_BLUE if i % 2 == 0 else WHITE
shp_pc = add_rect(s8, 0.3, y, 4.1, 0.43, bg_p)
add_textbox(s8, 0.38, y+0.04, 3.9, 0.35, crit, 10.5, False, DARK_GRAY)
shp_perc_note = add_rect(s8, 0.3, 5.55, 4.1, 0.4, LIME, radius=True)
add_shape_text(shp_perc_note, "All 8 absent → PE excluded. No D-dimer needed!", 10, True, WHITE)
# Wells
shp_wells = add_rect(s8, 4.65, 0.9, 4.3, 5.1, WHITE, ORANGE, Pt(2), radius=True)
add_textbox(s8, 4.75, 0.95, 4.1, 0.42, "WELLS SCORE — PE Probability", 12, True, ORANGE)
wells_criteria = [
("Clinical DVT signs", "3 pts"),
("PE most likely Dx", "3 pts"),
("HR > 100 bpm", "1.5 pts"),
("Immobility/surgery <4wks", "1.5 pts"),
("Prior DVT/PE", "1.5 pts"),
("Haemoptysis", "1 pt"),
("Active malignancy", "1 pt"),
]
for i, (crit_w, pts_w) in enumerate(wells_criteria):
y = 1.42 + i * 0.52
bg_w = LIGHT_BLUE if i % 2 == 0 else WHITE
shp_wc = add_rect(s8, 4.7, y, 4.2, 0.48, bg_w)
add_textbox(s8, 4.78, y+0.06, 2.9, 0.36, crit_w, 10, False, DARK_GRAY)
add_textbox(s8, 7.7, y+0.06, 1.1, 0.36, pts_w, 10, True, ORANGE, PP_ALIGN.RIGHT)
shp_wells_score = add_rect(s8, 4.7, 5.07, 4.2, 0.85, YELLOW_BG, ORANGE, Pt(1.5), radius=True)
add_textbox(s8, 4.78, 5.12, 4.0, 0.75,
"Score ≤4 + D-dimer negative → PE excluded\n"
"Score >4 → CTPA required\n"
"Age-adjusted D-dimer: age × 10 µg/L (if age >50)",
9.5, False, DARK_GRAY)
# BNP + Framingham
shp_bnp = add_rect(s8, 9.2, 0.9, 3.9, 2.6, WHITE, MED_BLUE, Pt(2), radius=True)
add_textbox(s8, 9.3, 0.95, 3.7, 0.42, "BNP / NT-proBNP (CHF vs non-cardiac)", 11, True, MED_BLUE)
bnp_text = ("BNP <100 pg/mL → NPV 90% (rules OUT CHF)\n"
"BNP >500 pg/mL → strongly supports CHF\n"
"NT-proBNP <300 → rules out CHF\n"
"NT-proBNP cutoffs by age:\n"
" <50y: >900 | 50-75y: >1800 | >75y: >1800")
add_textbox(s8, 9.3, 1.4, 3.7, 2.0, bnp_text, 10, False, DARK_GRAY)
shp_fram = add_rect(s8, 9.2, 3.6, 3.9, 2.3, WHITE, TEAL, Pt(2), radius=True)
add_textbox(s8, 9.3, 3.65, 3.7, 0.42, "FRAMINGHAM CRITERIA (CHF Diagnosis)", 11, True, TEAL)
fram_text = ("Major: PND, orthopnoea, raised JVP, crackles,\nS3 gallop, cardiomegaly, acute pulmonary oedema\n\n"
"Minor: Ankle oedema, nocturnal cough, DOE,\nhepatomegaly, pleural effusion, tachycardia\n\n"
"Diagnosis: 2 Major OR 1 Major + 2 Minor")
add_textbox(s8, 9.3, 4.1, 3.7, 1.75, fram_text, 9.5, False, DARK_GRAY)
# ═══════════════════════════════════════════════════════════════
# SLIDE 9 – Master Diagnostic Algorithm
# ═══════════════════════════════════════════════════════════════
s9 = prs.slides.add_slide(blank)
add_rect(s9, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s9, "MASTER DIAGNOSTIC ALGORITHM — ACUTE DYSPNEA IN ED", "Tintinalli's EM | Harrison's 22E | SFMU/FICS 2026")
bottom_bar(s9)
# Flowchart boxes
def flow_box(slide, x, y, w, h, text, bg, text_color=WHITE, font_size=11, bold=True, line_color=None):
lc = line_color if line_color else bg
shp = add_rect(slide, x, y, w, h, bg, lc, Pt(1.5), radius=True)
add_shape_text(shp, text, font_size, bold, text_color, PP_ALIGN.CENTER)
return shp
# Row 1 - Entry
flow_box(s9, 4.9, 0.92, 3.5, 0.55, "PATIENT ARRIVES WITH ACUTE DYSPNEA", DARK_BLUE, WHITE, 12, True)
# Arrow down
add_connector(s9, 6.67, 1.47, 6.67, 1.78, DARK_GRAY, Pt(2))
# Row 2 - Immediate
flow_box(s9, 3.8, 1.78, 5.7, 0.55, "IMMEDIATE: SpO2 + IV access + Monitor + O2 if SpO2 <92%", MED_BLUE, WHITE, 11, True)
add_connector(s9, 6.67, 2.33, 6.67, 2.63, DARK_GRAY, Pt(2))
# Row 3 - Is patient in danger?
shp_danger = flow_box(s9, 4.3, 2.63, 4.7, 0.55, "Is patient in IMMEDIATE DANGER?", RED, WHITE, 12, True)
# YES branch
add_connector(s9, 4.3, 2.9, 2.8, 2.9, RED, Pt(2))
add_connector(s9, 2.8, 2.9, 2.8, 3.35, RED, Pt(2))
flow_box(s9, 1.05, 3.35, 3.4, 0.95,
"YES: Immediate Action\n• Airway: RSI if needed (Ketamine)\n• O2 / NIV / HFNC\n• POCUS + ECG simultaneously",
RED, WHITE, 9.5, False)
# NO branch
add_connector(s9, 9.0, 2.9, 10.5, 2.9, GREEN, Pt(2))
add_connector(s9, 10.5, 2.9, 10.5, 3.35, GREEN, Pt(2))
flow_box(s9, 9.2, 3.35, 2.8, 0.95,
"NO: Focused assessment\n• History (onset, character)\n• Physical examination\n• Serial vital signs",
GREEN, WHITE, 9.5, False)
# YES/NO labels
add_textbox(s9, 2.5, 2.62, 0.7, 0.28, "YES", 10, True, RED, PP_ALIGN.CENTER)
add_textbox(s9, 9.05, 2.62, 0.7, 0.28, "NO", 10, True, GREEN, PP_ALIGN.CENTER)
add_connector(s9, 6.67, 3.18, 6.67, 3.55, DARK_GRAY, Pt(2))
# Row 4 - POCUS
flow_box(s9, 4.8, 3.55, 3.7, 0.5, "POCUS — BLUE Protocol", TEAL, WHITE, 12, True)
add_connector(s9, 6.67, 4.05, 6.67, 4.25, DARK_GRAY, Pt(2))
# Row 5 - POCUS branches (4 boxes)
pocus_results = [
(0.25, "A-lines\n(Asthma/COPD)", MED_BLUE),
(3.5, "B-lines\n(ADHF/Oedema)", RED),
(6.75, "No lung sliding\n(Pneumothorax)", ORANGE),
(10.0, "Consolidation\n(Pneumonia)", GREEN),
]
for (x_p, txt_p, col_p) in pocus_results:
add_connector(s9, x_p + 1.4, 4.25, x_p + 1.4, 4.45, col_p, Pt(1.5))
flow_box(s9, x_p, 4.45, 2.8, 0.7, txt_p, col_p, WHITE, 10, True)
add_connector(s9, 6.67, 4.25, 1.65, 4.25, DARK_GRAY, Pt(1.5))
add_connector(s9, 6.67, 4.25, 11.4, 4.25, DARK_GRAY, Pt(1.5))
add_connector(s9, 4.9, 4.25, 4.9, 4.45, DARK_GRAY, Pt(1.5))
add_connector(s9, 8.45, 4.25, 8.45, 4.45, DARK_GRAY, Pt(1.5))
# Row 6 - Biomarkers
add_connector(s9, 6.67, 5.15, 6.67, 5.35, DARK_GRAY, Pt(2))
flow_box(s9, 2.5, 5.35, 8.3, 0.52,
"BIOMARKERS: BNP/NT-proBNP | Troponin | D-dimer (age-adjusted) | Lactate | Full blood count | Metabolic panel",
DARK_BLUE, WHITE, 10.5, True)
add_connector(s9, 6.67, 5.87, 6.67, 6.07, DARK_GRAY, Pt(2))
flow_box(s9, 3.5, 6.07, 6.3, 0.5,
"DIAGNOSE & TREAT — Admit / Discharge / ICU based on severity",
DARK_BLUE, WHITE, 11, True)
# ═══════════════════════════════════════════════════════════════
# SLIDE 10 – Oxygenation Strategies
# ═══════════════════════════════════════════════════════════════
s10 = prs.slides.add_slide(blank)
add_rect(s10, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s10, "OXYGENATION & VENTILATION STRATEGIES", "GINA 2026 | SFMU/FICS 2026 | Tintinalli's EM")
bottom_bar(s10)
oxy_data = [
("Nasal Cannula", "Mild hypoxia\nSpO2 90-94%", "1-6 L/min\nFiO2 ~24-44%", MED_BLUE, "Standard first step"),
("Simple Face Mask", "SpO2 <90%\nNeeds higher FiO2", "6-10 L/min\nFiO2 ~40-60%", MED_BLUE, "Step up from NC"),
("Non-rebreather\nMask", "SpO2 <90%\nNeeds high FiO2", "10-15 L/min\nFiO2 ~80-100%", ORANGE, "Emergency hypoxia"),
("HFNC\n(High-Flow NC)", "Moderate-severe\nhypoxic resp. failure", "Up to 60 L/min\nFiO2 100%", TEAL, "~2 cmH2O PEEP per 10 L/min"),
("CPAP", "Cardiogenic\npulmonary oedema", "5-10 cmH2O\nFiO2 titrated", GREEN, "Reduces WOB; no expiratory support"),
("BiPAP / NIV", "COPD exacerbation\nCHF, mild-mod hypercapnia", "IPAP 12-20 / EPAP 4-8\nFiO2 titrated", GREEN, "Reduces intubation rates"),
("Endotracheal\nIntubation + MV", "Failure of above\nGCS <8 / impending arrest", "Ketamine (RSI)\nPermissive hypercapnia", RED, "Low TV 6-8 mL/kg | RR <10"),
]
headers = ["Modality", "Indication", "Settings / FiO2", "Notes"]
col_widths = [2.1, 2.6, 2.6, 2.5]
col_starts = [0.25, 2.4, 5.05, 7.7]
# header row
for j, (hdr, cw, cx) in enumerate(zip(headers, col_widths, col_starts)):
shp_hh = add_rect(s10, cx, 0.9, cw-0.05, 0.42, DARK_BLUE)
add_shape_text(shp_hh, hdr, 11, True, WHITE)
for i, (mod, ind, sett, col, note) in enumerate(oxy_data):
bg = LIGHT_BLUE if i % 2 == 0 else WHITE
row_y = 1.35 + i * 0.82
data_cells = [mod, ind, sett, note]
for j, (cell, cw, cx) in enumerate(zip(data_cells, col_widths, col_starts)):
shp_cell = add_rect(s10, cx, row_y, cw-0.05, 0.78, bg, MID_GRAY, Pt(0.3))
add_textbox(s10, cx+0.07, row_y+0.05, cw-0.15, 0.68, cell, 9.5, j==0, col if j==0 else DARK_GRAY)
add_textbox(s10, 10.3, 0.9, 2.85, 0.4, "KEY POINTS", 11, True, DARK_BLUE)
kp_items = [
"GINA 2026: O2 only if SpO2 <92%; target 92-95%",
"Avoid hyperoxia (drives ↑PaCO2 in COPD)",
"HFNC: generates 2 cmH2O PEEP per 10 L/min",
"NIV: reduces intubation in COPD/CHF",
"RSI drug of choice (bronchospasm): KETAMINE 1-2 mg/kg",
"Permissive hypercapnia in intubated asthmatic",
]
for i, kp in enumerate(kp_items):
shp_kp = add_rect(s10, 10.3, 1.35 + i * 0.95, 2.85, 0.85,
YELLOW_BG if i % 2 == 0 else WHITE,
ORANGE, Pt(1), radius=True)
add_textbox(s10, 10.38, 1.4 + i * 0.95, 2.68, 0.75, kp, 9, False, DARK_GRAY)
# ═══════════════════════════════════════════════════════════════
# SLIDE 11 – Specific Diagnoses Management
# ═══════════════════════════════════════════════════════════════
s11 = prs.slides.add_slide(blank)
add_rect(s11, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s11, "MANAGEMENT OF KEY DIAGNOSES", "Tintinalli's EM | Harrison's 22E | GINA 2026 | AHA/ACC 2026")
bottom_bar(s11)
dx_mgmt = [
("ADHF — Acute Decompensated HF", MED_BLUE,
"• IV furosemide 0.5-1 mg/kg\n"
"• IV/SL nitrates (preload + afterload ↓)\n"
"• CPAP/BiPAP (reduces intubation)\n"
"• BNP-guided therapy improves outcomes\n"
"• Avoid aggressive fluid in HFpEF"),
("Massive PE", RED,
"• UFH anticoagulation (preferred if thrombolysis candidate)\n"
"• Systemic thrombolysis: Alteplase 100 mg IV over 2 hrs\n"
"• Low-risk PE: discharge on DOAC (rivaroxaban/apixaban)\n"
"• AHA/ACC 2026: Activate PE Response Team (PERT)\n"
"• Catheter-directed therapy for sub-massive PE"),
("COPD Exacerbation", TEAL,
"• SABA + SAMA nebulisation\n"
"• Systemic steroids (prednisolone 30-40 mg)\n"
"• Controlled O2: target SpO2 88-92%\n"
"• BiPAP: first-line for moderate-severe (reduces intubation)\n"
"• Antibiotics if purulent sputum / CXR infiltrate"),
("Tension Pneumothorax", ORANGE,
"• DO NOT WAIT for CXR — clinical diagnosis\n"
"• Immediate needle decompression:\n"
" 2nd ICS, midclavicular line\n"
" OR 4th/5th ICS, anterior axillary line\n"
"• Followed by chest tube insertion"),
("Acute Asthma (GINA 2026)", GREEN,
"• Salbutamol 4-8 puffs/5 mg neb (every 20 min x3)\n"
"• Ipratropium 4 puffs with each SABA (first 3 doses)\n"
"• Systemic corticosteroids EARLY\n"
"• IV MgSO4 2g over 20 min (severe, PEF <25%)\n"
"• O2 only if SpO2 <92%; target 92-95%"),
("Anaphylaxis + Dyspnea", RED,
"• EPINEPHRINE FIRST (IM 0.3-0.5 mg)\n"
"• THEN bronchodilators (GINA 2026 NEW rule)\n"
"• Early airway management if angioedema progressing\n"
"• Antihistamines + steroids as adjuncts\n"
"• IV access + monitor continuously"),
]
for idx, (dx_title, col_dx, mgmt_text) in enumerate(dx_mgmt):
col_n = idx % 3
row_n = idx // 3
x_dx = 0.25 + col_n * 4.37
y_dx = 0.95 + row_n * 3.1
shp_dx = add_rect(s11, x_dx, y_dx, 4.2, 2.85, WHITE, col_dx, Pt(2), radius=True)
add_textbox(s11, x_dx+0.1, y_dx+0.05, 4.0, 0.42, dx_title, 11.5, True, col_dx)
add_textbox(s11, x_dx+0.1, y_dx+0.5, 4.0, 2.25, mgmt_text, 10, False, DARK_GRAY)
# ═══════════════════════════════════════════════════════════════
# SLIDE 12 – Disposition & High-Yield Points
# ═══════════════════════════════════════════════════════════════
s12 = prs.slides.add_slide(blank)
add_rect(s12, 0, 0, 13.333, 7.5, LIGHT_GRAY)
slide_header(s12, "DISPOSITION & HIGH-YIELD EXAM POINTS", "Tintinalli's EM | Harrison's 22E | GINA 2026 | SFMU/FICS 2026")
bottom_bar(s12)
# Disposition boxes
disp_data = [
("DISCHARGE (Low Risk)", GREEN,
"• SpO2 ≥95% on room air after treatment\n"
"• Symptoms resolved, cause identified and treated\n"
"• Reliable follow-up arranged\n"
"• No high-risk ECG changes or elevated troponin"),
("ADMIT (Ward)", ORANGE,
"• SpO2 <92% despite initial treatment\n"
"• Hemodynamic instability\n"
"• Persistently abnormal vital signs\n"
"• High-risk diagnoses requiring IV treatment"),
("ICU / HDU", RED,
"• Respiratory failure: NIV failing or intubated\n"
"• Hemodynamic compromise (shock)\n"
"• Rising PaCO2 + altered consciousness\n"
"• ARDS, massive PE, cardiac tamponade"),
]
for i, (disp_title, disp_col, disp_body) in enumerate(disp_data):
x_d = 0.25 + i * 4.37
shp_d = add_rect(s12, x_d, 0.9, 4.2, 2.5, WHITE, disp_col, Pt(2), radius=True)
add_textbox(s12, x_d+0.1, 0.95, 4.0, 0.45, disp_title, 12, True, disp_col)
add_textbox(s12, x_d+0.1, 1.44, 4.0, 1.9, disp_body, 10.5, False, DARK_GRAY)
# High yield exam points
shp_hy = add_rect(s12, 0.25, 3.55, 12.8, 0.42, DARK_BLUE, radius=True)
add_shape_text(shp_hy, "HIGH-YIELD EXAM POINTS", 14, True, WHITE)
hy_points = [
("Cardiac + pulmonary causes = 85% of all dyspnea (Harrison's 22E)", MED_BLUE),
("BNP <100 pg/mL → NPV 90% for CHF rule-out", GREEN),
("Platypnea = dyspnea WORSE sitting up → hepatopulmonary syndrome / left atrial myxoma / ASD", RED),
("Silent chest in asthma = life-threatening (no air movement, not 'improved')", RED),
("Normal PaCO2 during acute asthma attack = DANGER sign (patient fatiguing)", ORANGE),
("Pregnancy: PaCO2 40 mmHg = HYPERCARBIA (normal = 28-32 mmHg in pregnancy)", ORANGE),
("POCUS Lung point = pathognomonic for pneumothorax", TEAL),
("Efferent-reafferent mismatch = most intense dyspnea mechanism (Harrison's 22E viva)", MED_BLUE),
("SFMU/FICS 2026: CRP/PCT insufficient evidence to guide antibiotics in acute dyspnea + suspected LRTI", RED),
("GINA 2026: If anaphylaxis + asthma → epinephrine FIRST, then bronchodilator", ORANGE),
("PERC: all 8 absent in low-probability → no D-dimer, no CTPA needed", GREEN),
("RSI drug of choice in bronchospasm: KETAMINE 1-2 mg/kg IV", MED_BLUE),
]
cols_hy = 2
for i, (pt_txt, pt_col) in enumerate(hy_points):
col_h = i % cols_hy
row_h = i // cols_hy
x_h = 0.25 + col_h * 6.55
y_h = 4.04 + row_h * 0.49
bg_h = WHITE if row_h % 2 == 0 else LIGHT_BLUE
shp_h = add_rect(s12, x_h, y_h, 6.4, 0.44, bg_h, pt_col, Pt(1))
add_textbox(s12, x_h+0.1, y_h+0.04, 0.3, 0.36, "★", 11, True, pt_col, PP_ALIGN.CENTER)
add_textbox(s12, x_h+0.42, y_h+0.04, 5.8, 0.36, pt_txt, 9.5, False, DARK_GRAY)
# ═══════════════════════════════════════════════════════════════
# SLIDE 13 – References & Summary
# ═══════════════════════════════════════════════════════════════
s13 = prs.slides.add_slide(blank)
add_rect(s13, 0, 0, 13.333, 7.5, DARK_BLUE)
# accent
add_rect(s13, 0, 2.0, 13.333, 0.06, MED_BLUE)
add_textbox(s13, 0.5, 0.3, 12.3, 0.75,
"REFERENCES & SOURCES",
font_size=28, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
refs = [
("Harrison's Principles of Internal Medicine 22E (2025)", "Chapter 39 — Dyspnea: Definition, Mechanisms, Differential Diagnosis, Assessment, CPET, Treatment; mMRC Scale, Table 34-3"),
("Tintinalli's Emergency Medicine — A Comprehensive Study", "Chapters 53, 59 — ADHF, Asthma/COPD; POCUS (B-lines, lung sliding); Chapter 25 — Palliative dyspnea; Chapter 24 — IVDU dyspnea"),
("SFMU/FICS Guidelines 2026", "Ann Intensive Care 2026 Jan — Guidelines for Initial Assessment of Respiratory Distress in the ED; RR measurement, biomarker guidance"),
("GINA 2026 Strategy Report", "May 5, 2026 — Acute asthma management; O2 targets; anaphylaxis+asthma: epinephrine first rule; ICS-formoterol for mild exacerbation"),
("AHA/ACC/ACEP 2026 PE Guideline", "Circulation 2026 — Risk stratification, ED discharge for low-risk PE, PERT, post-PE dyspnea follow-up"),
("Taheri et al., Eur J Emerg Med 2025 (PMID 39630617)", "POCUS in prehospital non-trauma acute dyspnea — systematic review and meta-analysis"),
("Russell et al., Am J Emerg Med 2024 (PMID 38522242)", "Prehospital lung ultrasound for ADHF — systematic review/meta-analysis"),
("Jeffers et al., J Emerg Med 2025 (PMID 40845678)", "POCUS utility for pulmonary oedema — systematic review"),
("Santus et al., Intern Emerg Med 2023 (PMID 37266791)", "Acute dyspnea in the emergency department — comprehensive clinical review"),
]
for i, (title_r, desc_r) in enumerate(refs):
y_r = 2.18 + i * 0.55
bg_r = RGBColor(0x12, 0x37, 0x6B) if i % 2 == 0 else RGBColor(0x0A, 0x29, 0x5C)
shp_r = add_rect(s13, 0.25, y_r, 12.8, 0.5, bg_r, MED_BLUE, Pt(0.5))
add_textbox(s13, 0.35, y_r+0.04, 4.5, 0.42, title_r, 9.5, True, LIGHT_BLUE)
add_textbox(s13, 4.9, y_r+0.04, 8.0, 0.42, desc_r, 9, False, WHITE)
# ═══════════════════════════════════════════════════════════════
# SAVE
# ═══════════════════════════════════════════════════════════════
output_path = "/tmp/workspace/acute-dyspnea-ppt/Acute_Dyspnea_EM_Teaching.pptx"
buf = BytesIO()
prs.save(buf)
with open(output_path, "wb") as f:
f.write(buf.getvalue())
print(f"Saved: {output_path}")
print(f"Slides: {len(prs.slides)}")