Teach me heart murmurs from absolute beginner level, as if I am a 10-year-old student with no previous knowledge. Teach me step by step, in the correct learning order. Do not assume I already understand the cardiac cycle, heart valves, heart sounds, systole, diastole, or blood flow. Use very simple English first, then gradually introduce the correct medical terms
RIGHT side | LEFT side
___________ | ___________
| | | | |
Top: | Right | | | Left |
| Atrium | | | Atrium |
|___________| | |___________|
β | β
___________ | ___________
| | | | |
Bottom: | Right | | | Left |
| Ventricle | | | Ventricle |
|___________| | |___________|
β β
To lungs To the body
Medical term check: Atrium (plural: atria) = top receiving room. Ventricle = bottom pumping room.
| Valve name | Where it is | What it does |
|---|---|---|
| Mitral valve | Between left atrium and left ventricle | Lets blood flow down into the left pumping room |
| Tricuspid valve | Between right atrium and right ventricle | Lets blood flow down into the right pumping room |
| Aortic valve | At the exit of the left ventricle | Opens to let blood rush out to the body |
| Pulmonary valve | At the exit of the right ventricle | Opens to let blood rush out to the lungs |
Memory trick: "My Tricycle" = Mitral, Tricuspid = the two valves in the middle of the heart. "Apes Prefer" = Aortic, Pulmonary = the two valves at the exits.
Simple summary:
- Systole = the squeeze = heart pushing blood OUT
- Diastole = the relaxation = heart filling WITH blood
LUB -----(squeeze/systole)------ DUB -----(relax/diastole)------ LUB ...
β β
Mitral + Tricuspid close Aortic + Pulmonary close
(S1) (S2)
Medical terms: S1 = first heart sound. S2 = second heart sound. Together, they define the two phases of the cardiac cycle (kar-dee-ak SY-kull) - the full sequence of one heartbeat.
Medical definition: A heart murmur is an audible vibration caused by turbulent blood flow, heard with a stethoscope between the normal heart sounds. - Harrison's Principles of Internal Medicine, 22nd Edition
Simple way to remember:
- Too narrow = stenosis
- Too leaky = regurgitation
- A hole = septal defect
LUB -----(systole)------- DUB -----(diastole)------ LUB
S1 S2 S1
Diastolic murmurs are always significant - they always indicate a structural problem and require investigation. - Harrison's Principles of Internal Medicine, 22nd Edition

| Grade | What the doctor experiences |
|---|---|
| 1/6 | Very faint - you almost cannot hear it. Requires very careful listening. |
| 2/6 | Soft, but easy to hear once you find it. |
| 3/6 | Clearly loud. No vibration felt on the chest. |
| 4/6 | Loud AND you can feel a vibration on the chest wall (called a thrill). |
| 5/6 | Very loud - heard even with just the edge of the stethoscope touching the chest. |
| 6/6 | Extremely loud - you can hear it with the stethoscope held slightly off the chest. |
Grades 3 and above usually suggest a structural problem with the heart.


| Term | Simple meaning | Medical word |
|---|---|---|
| Heart rooms | Chambers of the heart | Atria (top), Ventricles (bottom) |
| Heart doors | One-way gates for blood | Valves |
| Squeeze phase | Heart pushing blood out | Systole |
| Relaxation phase | Heart filling with blood | Diastole |
| "Lub" | First valve closure sound | S1 (First heart sound) |
| "Dub" | Second valve closure sound | S2 (Second heart sound) |
| Whooshing extra sound | Turbulent blood flow | Murmur |
| Narrowed valve | Doesn't open fully | Stenosis |
| Leaky valve | Doesn't close fully | Regurgitation |
| Hole between chambers | Abnormal blood path | Septal defect |
| Harmless murmur | Normal fast-flow noise | Innocent/functional murmur |
| Murmur during squeeze | Between S1 and S2 | Systolic murmur |
| Murmur during relaxation | Between S2 and S1 | Diastolic murmur |
| Murmur at all times | Throughout the cycle | Continuous murmur |
| Murmur loudness scale | 1 (faintest) to 6 (loudest) | Grading (I-VI/VI) |
| Heart sound ultrasound | Imaging the moving heart | Echocardiography (echo) |
Generate a quick reference card or study guide for murmur characteristics
ls -la /tmp/workspace/ 2>/dev/null || echo "workspace empty or not found"
mkdir -p /tmp/workspace/murmur-reference
~/murmur-reference/build_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.platypus import (
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer,
HRFlowable, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame
from reportlab.lib.colors import HexColor
import os
OUTPUT = "/tmp/workspace/murmur-reference/Heart_Murmur_Quick_Reference.pdf"
# ββ Palette ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
NAVY = HexColor("#1A2B4A")
TEAL = HexColor("#0D7377")
TEAL_LIGHT= HexColor("#14BDBE")
CREAM = HexColor("#F7F9FC")
YELLOW = HexColor("#FFF3CD")
YELLOW_BD = HexColor("#E6A817")
RED_BG = HexColor("#FDF0F0")
RED_BD = HexColor("#C0392B")
GREEN_BG = HexColor("#EEF8F0")
GREEN_BD = HexColor("#27AE60")
PURPLE_BG = HexColor("#F3EEF8")
PURPLE_BD = HexColor("#8E44AD")
BLUE_BG = HexColor("#EEF4FD")
BLUE_BD = HexColor("#2980B9")
ORANGE_BG = HexColor("#FFF5EC")
ORANGE_BD = HexColor("#E67E22")
GREY_LIGHT= HexColor("#E8ECF0")
WHITE = colors.white
BLACK = colors.black
# ββ Styles ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
styles = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
TITLE_STYLE = S("title",
fontSize=22, fontName="Helvetica-Bold",
textColor=WHITE, alignment=TA_CENTER, leading=28)
SUBTITLE_STYLE = S("subtitle",
fontSize=10, fontName="Helvetica",
textColor=HexColor("#B0D0E8"), alignment=TA_CENTER, leading=14)
SEC_HEADER = S("sec_header",
fontSize=11, fontName="Helvetica-Bold",
textColor=WHITE, alignment=TA_CENTER, leading=15)
CELL_BOLD = S("cell_bold",
fontSize=8.5, fontName="Helvetica-Bold",
textColor=NAVY, alignment=TA_LEFT, leading=11)
CELL_NORMAL = S("cell_normal",
fontSize=8, fontName="Helvetica",
textColor=HexColor("#2C3E50"), alignment=TA_LEFT, leading=10.5)
CELL_CENTER = S("cell_center",
fontSize=8, fontName="Helvetica",
textColor=HexColor("#2C3E50"), alignment=TA_CENTER, leading=10.5)
SMALL_ITALIC = S("small_italic",
fontSize=7.5, fontName="Helvetica-Oblique",
textColor=HexColor("#5D6D7E"), alignment=TA_CENTER, leading=10)
LEGEND_STYLE = S("legend",
fontSize=7.5, fontName="Helvetica",
textColor=HexColor("#2C3E50"), alignment=TA_LEFT, leading=11)
FOOTER_STYLE = S("footer",
fontSize=7, fontName="Helvetica",
textColor=HexColor("#95A5A6"), alignment=TA_CENTER, leading=9)
def p(text, style=CELL_NORMAL):
return Paragraph(text, style)
def pb(text):
return Paragraph(text, CELL_BOLD)
def pc(text):
return Paragraph(text, CELL_CENTER)
# ββ Helper: colored badge paragraph ββββββββββββββββββββββββββββββββββββββββββ
def badge(text, bg, bd):
style = S(f"badge_{text[:4]}",
fontSize=7.5, fontName="Helvetica-Bold",
textColor=bd, alignment=TA_CENTER, leading=10,
backColor=bg, borderPadding=(2,4,2,4),
borderColor=bd, borderWidth=0.5, borderRadius=3)
return Paragraph(text, style)
# ββ Document setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=14*mm, rightMargin=14*mm,
topMargin=14*mm, bottomMargin=14*mm,
title="Heart Murmur Quick Reference Card",
author="Orris Medical",
)
W, H = A4
USABLE_W = W - 28*mm
story = []
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# HEADER BANNER
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
header_data = [[
Paragraph("π« HEART MURMUR QUICK REFERENCE CARD", TITLE_STYLE),
]]
header_tbl = Table(header_data, colWidths=[USABLE_W])
header_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("ROUNDEDCORNERS", [6]),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING",(0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
]))
story.append(header_tbl)
subtitle_tbl = Table([[
Paragraph("Timing Β· Intensity Β· Location Β· Quality Β· Key Causes | "
"Sources: Harrison's 22E Β· Guyton & Hall Β· Fuster & Hurst 15E", SUBTITLE_STYLE)
]], colWidths=[USABLE_W])
subtitle_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), TEAL),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
]))
story.append(subtitle_tbl)
story.append(Spacer(1, 5*mm))
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 1 β CARDIAC CYCLE REMINDER
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def section_title(text, bg=TEAL):
t = Table([[p(text, SEC_HEADER)]], colWidths=[USABLE_W])
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), bg),
("TOPPADDING", (0,0),(-1,-1), 4),
("BOTTOMPADDING", (0,0),(-1,-1), 4),
("LEFTPADDING", (0,0),(-1,-1), 8),
]))
return t
story.append(section_title("1 Β· THE CARDIAC CYCLE AT A GLANCE"))
story.append(Spacer(1, 2*mm))
cycle_data = [
[pb("Phase"), pb("What happens"), pb("Valves OPEN"), pb("Valves CLOSED"), pb("Heart sounds")],
[p("SYSTOLE\n(squeeze)"),
p("Ventricles contract;\nblood ejected"),
p("Aortic, Pulmonary"),
p("Mitral, Tricuspid"),
p("S1 at start (AV valves close)\nS2 at end (semilunar valves close)")],
[p("DIASTOLE\n(relax/fill)"),
p("Ventricles relax;\nblood fills from atria"),
p("Mitral, Tricuspid"),
p("Aortic, Pulmonary"),
p("Next S1 marks the end")],
]
cycle_tbl = Table(cycle_data, colWidths=[22*mm, 42*mm, 38*mm, 38*mm, 43*mm])
cycle_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,0), 8),
("BACKGROUND", (0,1), (-1,1), HexColor("#EEF4FD")),
("BACKGROUND", (0,2), (-1,2), HexColor("#EEF8F0")),
("ROWBACKGROUNDS",(0,1),(-1,-1), [HexColor("#EEF4FD"), HexColor("#EEF8F0")]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BDC3C7")),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING",(0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(cycle_tbl)
story.append(Spacer(1, 5*mm))
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 2 β MURMUR GRADING SCALE
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
story.append(section_title("2 Β· MURMUR INTENSITY GRADING (Levine Scale I β VI)"))
story.append(Spacer(1, 2*mm))
grade_data = [
[pb("Grade"), pb("What the examiner experiences"), pb("Thrill?"), pb("Clinical significance")],
[pc("I / VI"), p("Very faint β heard only with intense concentration"), pc("No"), p("May be innocent")],
[pc("II / VI"), p("Soft but immediately audible"), pc("No"), p("Often innocent")],
[pc("III / VI"), p("Moderately loud"), pc("No"), p("Usually pathological")],
[pc("IV / VI"), p("Loud"), pc("YES"), p("Pathological")],
[pc("V / VI"), p("Very loud β heard with stethoscope edge on chest"), pc("YES"), p("Pathological")],
[pc("VI / VI"), p("Audible with stethoscope OFF the chest"), pc("YES"), p("Pathological")],
]
grade_tbl = Table(grade_data, colWidths=[18*mm, 75*mm, 18*mm, 72*mm])
grade_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,0), 8),
("ROWBACKGROUNDS",(0,1),(-1,-1), [WHITE, GREY_LIGHT]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BDC3C7")),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING",(0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("ALIGN", (0,0), (0,-1), "CENTER"),
("ALIGN", (2,0), (2,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
# Highlight grade III+ rows
("BACKGROUND", (0,3), (-1,3), HexColor("#FFF9E6")),
("BACKGROUND", (0,4), (-1,4), HexColor("#FFF0E6")),
("BACKGROUND", (0,5), (-1,5), HexColor("#FDE8E8")),
("BACKGROUND", (0,6), (-1,6), HexColor("#FADADD")),
]))
story.append(grade_tbl)
story.append(Paragraph("β
Grade β₯ III usually indicates structural heart disease. Grade β₯ III systolic, any holosystolic/late-systolic, or any diastolic murmur β Echocardiography indicated.", SMALL_ITALIC))
story.append(Spacer(1, 5*mm))
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 3 β MAIN MURMURS REFERENCE TABLE
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
story.append(section_title("3 Β· MAJOR MURMURS β CHARACTERISTICS AT A GLANCE"))
story.append(Spacer(1, 2*mm))
# Column widths
cw = [28*mm, 20*mm, 20*mm, 22*mm, 22*mm, 20*mm, 31*mm]
header_row = [
pb("Condition"),
pb("Timing"),
pb("Configuration"),
pb("Quality"),
pb("Best Heard"),
pb("Radiation"),
pb("Key Clue / Cause"),
]
def row(condition, timing, config, quality, location, radiation, clue,
bg=WHITE, bd=HexColor("#BDC3C7")):
return ([
p(condition, CELL_BOLD),
pc(timing),
pc(config),
p(quality),
p(location),
p(radiation),
p(clue),
], bg, bd)
murmur_rows = [
# SYSTOLIC
row("Aortic Stenosis (AS)",
"Systolic", "Crescendo-\nDecrescendo", "Harsh / rough", "R upper sternal border (RUSB)",
"Neck (carotids), apex (Gallavardin)", "Calcified valve (elderly) or bicuspid AoV", bg=RED_BG),
row("Mitral Regurgitation (MR)",
"Holosystolic\n(pansystolic)", "Plateau", "Blowing", "Apex (mitral area)",
"Left axilla / back", "MVP, rheumatic Fx, ischemia, IE", bg=RED_BG),
row("Tricuspid Regurgitation (TR)",
"Holosystolic", "Plateau", "Blowing", "Left lower sternal border (LLSB)",
"Increases with inspiration\n(Carvallo's sign)", "RV dilation, IE (IV drug use)", bg=RED_BG),
row("Pulmonary Stenosis (PS)",
"Systolic", "Crescendo-\nDecrescendo", "Harsh", "Left upper sternal border (LUSB)",
"Left shoulder / back", "Congenital; preceded by ejection click", bg=RED_BG),
row("VSD (small)",
"Holosystolic", "Plateau", "Harsh / loud", "Lower left sternal border",
"Right of sternum", "Congenital hole; louder = smaller defect\n(Maladie de Roger)", bg=RED_BG),
row("HCM / HOCM",
"Systolic", "Crescendo-\nDecrescendo", "Harsh", "LLSB / between apex & LLSB",
"No radiation to neck\n(unlike AS)", "β with Valsalva / standing; β with squatting", bg=RED_BG),
# DIASTOLIC
row("Aortic Regurgitation (AR)",
"Early diastolic", "Decrescendo", "High-pitched\nblowing", "Left sternal border (LSB), leaning forward",
"Apex (Austin Flint murmur)", "Aortic root dilation, IE, rheumatic fever", bg=BLUE_BG),
row("Mitral Stenosis (MS)",
"Mid-to-late\ndiastolic", "Rumble\n(low-pitched)", "Low rumble", "Apex (bell of stethoscope)",
"Does not radiate", "Rheumatic fever; opening snap (OS) precedes rumble", bg=BLUE_BG),
row("Tricuspid Stenosis (TS)",
"Mid-diastolic", "Rumble", "Low rumble", "LLSB",
"Increases with inspiration", "Rheumatic fever; usually with MS", bg=BLUE_BG),
row("Pulmonary Regurgitation (PR)",
"Early diastolic", "Decrescendo", "High-pitched\nblowing", "LUSB",
"None", "Pulmonary HTN (Graham Steell murmur)", bg=BLUE_BG),
# CONTINUOUS
row("Patent Ductus Arteriosus (PDA)",
"Continuous\n(systole + diastole)", "Waxes & wanes", "Machinery\nmurmur", "Left infraclavicular / LUSB",
"Left shoulder", "Premature birth, congenital; peaks at S2", bg=GREEN_BG),
]
all_rows = [header_row]
for r, bg, bd in murmur_rows:
all_rows.append(r)
main_tbl = Table(all_rows, colWidths=cw, repeatRows=1)
ts = [
("BACKGROUND", (0,0), (-1,0), NAVY),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 7.5),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BDC3C7")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]
# Row backgrounds
for i, (r, bg, bd) in enumerate(murmur_rows, start=1):
ts.append(("BACKGROUND", (0,i), (-1,i), bg))
# Section separator lines
ts.append(("LINEABOVE", (0,7), (-1,7), 1.5, BLUE_BD)) # start of diastolic
ts.append(("LINEABOVE", (0,11), (-1,11), 1.5, GREEN_BD)) # start of continuous
main_tbl.setStyle(TableStyle(ts))
story.append(main_tbl)
# Legend
legend_items = [
("β Red rows", "Systolic murmurs", RED_BG),
("β Blue rows", "Diastolic murmurs", BLUE_BG),
("β Green rows", "Continuous murmurs", GREEN_BG),
]
legend_data = [[
Paragraph(f'<font color="{bd.hexval()}">{label}</font> = {desc}', LEGEND_STYLE)
for label, desc, bd in legend_items
]]
legend_tbl = Table(legend_data, colWidths=[USABLE_W/3]*3)
legend_tbl.setStyle(TableStyle([
("TOPPADDING", (0,0),(-1,-1), 3),
("BOTTOMPADDING",(0,0),(-1,-1), 3),
("LEFTPADDING", (0,0),(-1,-1), 5),
]))
story.append(legend_tbl)
story.append(Spacer(1, 5*mm))
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 4 β MURMUR CONFIGURATION SHAPES
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
story.append(section_title("4 Β· MURMUR CONFIGURATION SHAPES"))
story.append(Spacer(1, 2*mm))
config_data = [
[pb("Shape name"), pb("Sound pattern"), pb("Visual shape"), pb("Example condition")],
[p("Crescendo"), p("Gets louder β"), p("ββΆ /"), p("Presystolic MS (atrial systole)")],
[p("Decrescendo"), p("Gets softer β"), p("ββΆ \\"), p("Aortic or Pulmonary Regurgitation")],
[p("Crescendo-Decrescendo"), p("Louder then softer (diamond)"), p("ββΆ /\\"), p("Aortic Stenosis, Pulmonary Stenosis")],
[p("Plateau (Holosystolic)"),p("Same loudness throughout"), p("ββΆ βββ"), p("MR, TR, small VSD")],
[p("Machinery"), p("Waxes/wanes; peaks near S2"), p("ββΆ /\\/\\ (continuous)"),p("Patent Ductus Arteriosus (PDA)")],
]
config_tbl = Table(config_data, colWidths=[42*mm, 50*mm, 40*mm, 51*mm])
config_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8),
("ROWBACKGROUNDS",(0,1),(-1,-1), [WHITE, GREY_LIGHT]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BDC3C7")),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(config_tbl)
story.append(Spacer(1, 5*mm))
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 5 β BEDSIDE MANEUVER EFFECTS
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
story.append(section_title("5 Β· BEDSIDE MANEUVERS & THEIR EFFECTS ON MURMURS"))
story.append(Spacer(1, 2*mm))
maneuver_data = [
[pb("Maneuver"), pb("Physiology"), pb("β Murmur"), pb("β Murmur")],
[p("Valsalva (strain phase)"),
p("β venous return β β preload β smaller LV"),
p("HCM (outflow obstruction β)\nMVP click moves earlier"),
p("AS, MR, VSD, TR (all β)")],
[p("Squatting β Standing"),
p("Squatting β preload & afterload"),
p("AS, MR, VSD, TR (squat β)"),
p("HCM (squat β)")],
[p("Standing β Squatting"),
p("Squatting β venous return"),
p("AS, MR, TR (squat β)"),
p("HCM, MVP (squat β)")],
[p("Inspiration (Mueller)"),
p("β venous return to right heart"),
p("Right-sided murmurs\n(TR, TS, PS, PR) β Carvallo sign"),
p("Left-sided murmurs (relatively)")],
[p("Leg Raise (passive)"),
p("β venous return β β preload"),
p("Most left-sided murmurs"),
p("HCM")],
[p("Amyl Nitrite (inhale)"),
p("β afterload β β heart rate"),
p("AS, HCM, MS (rumble), PS"),
p("MR, AR, VSD")],
[p("Hand-grip (isometric)"),
p("β afterload β β SVR"),
p("MR, AR, VSD"),
p("AS, HCM")],
]
maneuver_tbl = Table(maneuver_data, colWidths=[42*mm, 48*mm, 44*mm, 49*mm])
maneuver_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 7.5),
("ROWBACKGROUNDS",(0,1),(-1,-1), [WHITE, GREY_LIGHT]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BDC3C7")),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "TOP"),
# Color β column green, β column red
("TEXTCOLOR", (2,1), (2,-1), HexColor("#1A7A2B")),
("TEXTCOLOR", (3,1), (3,-1), HexColor("#C0392B")),
]))
story.append(maneuver_tbl)
story.append(Spacer(1, 5*mm))
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 6 β INNOCENT vs PATHOLOGICAL + MNEMONIC BOX
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Two-column layout: left = innocent vs pathological, right = mnemonics
left_data = [
[pb("Innocent Murmur"), pb("Pathological Murmur")],
[p("Grade I-II/VI"), p("Grade III/VI or above")],
[p("Systolic ONLY"), p("Any diastolic or continuous")],
[p("Short duration"), p("Holosystolic / late systolic")],
[p("No radiation"), p("Radiation present")],
[p("No other cardiac signs"), p("Thrill, click, S3, S4, loud S2")],
[p("Asymptomatic child/young adult"), p("Symptoms: dyspnoea, syncope, chest pain")],
[p("Varies with posture/respiration"), p("Associated structural findings on echo")],
]
left_tbl = Table(left_data, colWidths=[42*mm, 45*mm])
left_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,0), GREEN_BG),
("BACKGROUND", (1,0), (1,0), RED_BG),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 7.5),
("ROWBACKGROUNDS",(0,1),(-1,-1), [WHITE, GREY_LIGHT]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BDC3C7")),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
mnemonic_text = """<b>MEMORY AIDS</b>
<b>Valve locations on chest:</b>
<b>A</b>ll (<b>A</b>ortic) = 2nd R ICS parasternal
<b>P</b>atients (<b>P</b>ulmonary) = 2nd L ICS parasternal
<b>E</b>at (<b>E</b>rb's point) = 3rd L ICS β AR best heard here
<b>T</b>accos (<b>T</b>ricuspid) = 4th/5th L ICS parasternal
<b>M</b>ightily (<b>M</b>itral) = 5th ICS midclavicular (apex)
<b>HCM vs AS differentiator:</b>
HCM β with Valsalva / standing
AS does NOT change with Valsalva
<b>Right-sided murmur clue:</b>
All right-sided murmurs louder on INSPIRATION
(Carvallo's sign)
<b>Diastolic rule:</b>
Any diastolic murmur = ALWAYS pathological
β Order echo without hesitation"""
mnemonic_style = S("mnemo",
fontSize=7.5, fontName="Helvetica",
textColor=HexColor("#2C3E50"), alignment=TA_LEFT, leading=11)
mnemonic_para = Paragraph(mnemonic_text, mnemonic_style)
mnemonic_tbl = Table([[mnemonic_para]], colWidths=[90*mm])
mnemonic_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), HexColor("#FFFDE7")),
("BOX", (0,0), (-1,-1), 1, YELLOW_BD),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 7),
("RIGHTPADDING", (0,0), (-1,-1), 7),
]))
story.append(section_title("6 Β· INNOCENT vs PATHOLOGICAL + MEMORY AIDS"))
story.append(Spacer(1, 2*mm))
two_col = Table([[left_tbl, mnemonic_tbl]],
colWidths=[90*mm, 93*mm],
hAlign="LEFT")
two_col.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING",(0,0), (-1,-1), 0),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING",(0,0),(-1,-1), 0),
("INNERGRID", (0,0), (-1,-1), 0, WHITE),
]))
story.append(two_col)
story.append(Spacer(1, 5*mm))
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 7 β AUSCULTATION AREAS MINI-TABLE
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
story.append(section_title("7 Β· AUSCULTATION AREAS"))
story.append(Spacer(1, 2*mm))
ausc_data = [
[pb("Area"), pb("Location on chest"), pb("Valve / Murmur best heard here")],
[p("Aortic area"), p("2nd right intercostal space (ICS), right sternal border"), p("Aortic stenosis (AS)")],
[p("Pulmonary area"), p("2nd left ICS, left sternal border"), p("Pulmonary stenosis (PS); PDA")],
[p("Erb's point"), p("3rd left ICS, left sternal border"), p("Aortic regurgitation (AR) β best heard leaning forward, breath held in expiration")],
[p("Tricuspid area"), p("4thβ5th left ICS, left sternal border"), p("Tricuspid regurgitation (TR) and stenosis (TS); VSD")],
[p("Mitral area\n(Apex)"), p("5th left ICS, midclavicular line β the point of maximal impulse (PMI)"), p("Mitral regurgitation (MR), mitral stenosis (MS)")],
]
ausc_tbl = Table(ausc_data, colWidths=[35*mm, 73*mm, 75*mm])
ausc_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 7.5),
("ROWBACKGROUNDS",(0,1),(-1,-1), [WHITE, GREY_LIGHT]),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#BDC3C7")),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(ausc_tbl)
story.append(Spacer(1, 4*mm))
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# FOOTER
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
story.append(HRFlowable(width=USABLE_W, thickness=0.5, color=HexColor("#BDC3C7")))
story.append(Spacer(1, 1*mm))
story.append(Paragraph(
"Sources: Harrison's Principles of Internal Medicine 22E (2025, McGraw Hill) Β· "
"Guyton and Hall Textbook of Medical Physiology Β· "
"Fuster and Hurst's The Heart 15E Β· Goldman-Cecil Medicine | "
"Generated by Orris Medical AI Β· July 2026 | For educational use only.",
FOOTER_STYLE))
# ββ Build βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
doc.build(story)
print(f"PDF saved: {OUTPUT}")
cd /tmp/workspace/murmur-reference && python build_pdf.py 2>&1
ls -lh /tmp/workspace/murmur-reference/Heart_Murmur_Quick_Reference.pdf
Heart Murmur Quick Reference
PDF Document Β· PDF
Generate an interactive diagnostic flowchart in PDF or PowerPoint format
~/murmur-reference/build_flowchart.py
"""
Heart Murmur Interactive Diagnostic Flowchart
Built with python-pptx | Orris Medical AI
Each slide = one decision node; YES/NO buttons hyperlink to the next slide.
"""
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_TYPE
from pptx.oxml.ns import qn
from lxml import etree
from io import BytesIO
# ββ Palette ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
NAVY = RGBColor(0x1A, 0x2B, 0x4A)
TEAL = RGBColor(0x0D, 0x73, 0x77)
TEAL_L = RGBColor(0x14, 0xBD, 0xBE)
SKY = RGBColor(0xE8, 0xF4, 0xFD)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
GREY_DARK = RGBColor(0x44, 0x55, 0x66)
GREY_LITE = RGBColor(0xF0, 0xF4, 0xF8)
GREEN = RGBColor(0x27, 0xAE, 0x60)
GREEN_L = RGBColor(0xEE, 0xF8, 0xF0)
RED = RGBColor(0xC0, 0x39, 0x2B)
RED_L = RGBColor(0xFD, 0xF0, 0xF0)
AMBER = RGBColor(0xE6, 0x7E, 0x22)
AMBER_L = RGBColor(0xFF, 0xF5, 0xEC)
PURPLE = RGBColor(0x8E, 0x44, 0xAD)
PURPLE_L = RGBColor(0xF3, 0xEE, 0xF8)
BLUE = RGBColor(0x29, 0x80, 0xB9)
BLUE_L = RGBColor(0xEB, 0xF5, 0xFB)
YELLOW = RGBColor(0xF3, 0x9C, 0x12)
YELLOW_L = RGBColor(0xFF, 0xFB, 0xE6)
# ββ Presentation setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
BLANK = prs.slide_layouts[6]
SW = prs.slide_width
SH = prs.slide_height
# ββ Low-level hyperlink helper βββββββββββββββββββββββββββββββββββββββββββββββββ
def add_slide_hyperlink(shape, slide_index_0based, prs_obj):
"""
Add a slide-to-slide hyperlink on a shape by injecting the required XML.
slide_index_0based: 0-based index of the TARGET slide in prs.slides.
"""
rId = shape.part.relate_to(
prs_obj.slides[slide_index_0based].part,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"
)
sp_elem = shape._element
# Find or create hlinkClick
# For a shape, click action lives in sp > spPr > ...
# Better: use nvSpPr > cNvPr > hlinkClick (Office convention)
cNvPr = sp_elem.find(".//" + qn("p:cNvPr"))
if cNvPr is None:
cNvPr = sp_elem.find(".//" + qn("p:nvSpPr") + "/" + qn("p:cNvPr"))
# Use hlinkClick on the non-visual props
hl = etree.SubElement(cNvPr, qn("a:hlinkClick"))
hl.set(qn("r:id"), rId)
return rId
def rgb_to_hex(rgb: RGBColor) -> str:
return f"{rgb[0]:02X}{rgb[1]:02X}{rgb[2]:02X}"
# ββ Drawing helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def bg(slide, color: RGBColor):
"""Fill slide background."""
background = slide.background
fill = background.fill
fill.solid()
fill.fore_color.rgb = color
def shape_box(slide, x, y, w, h, fill_rgb, border_rgb=None, border_pt=1.5,
radius=True, shadow=False):
from pptx.enum.shapes import MSO_SHAPE
shape_type = MSO_SHAPE.ROUNDED_RECTANGLE if radius else MSO_SHAPE.RECTANGLE
shp = slide.shapes.add_shape(shape_type, x, y, w, h)
shp.fill.solid()
shp.fill.fore_color.rgb = fill_rgb
if border_rgb:
shp.line.color.rgb = border_rgb
shp.line.width = Pt(border_pt)
else:
shp.line.fill.background()
shp.shadow.inherit = False
return shp
def txt_in_shape(shp, text, font_size, font_rgb, bold=False, italic=False,
align=PP_ALIGN.CENTER, wrap=True, anchor=MSO_ANCHOR.MIDDLE,
second_line=None, second_size=None, second_rgb=None, second_bold=False):
tf = shp.text_frame
tf.word_wrap = wrap
tf.vertical_anchor = anchor
p = tf.paragraphs[0]
p.alignment = align
run = p.add_run()
run.text = text
run.font.size = Pt(font_size)
run.font.color.rgb = font_rgb
run.font.bold = bold
run.font.italic = italic
if second_line:
p2 = tf.add_paragraph()
p2.alignment = align
r2 = p2.add_run()
r2.text = second_line
r2.font.size = Pt(second_size or font_size - 2)
r2.font.color.rgb = second_rgb or font_rgb
r2.font.bold = second_bold
return tf
def add_label(slide, text, x, y, w, h, font_size=9, font_rgb=None, bold=False,
align=PP_ALIGN.CENTER):
font_rgb = font_rgb or GREY_DARK
tb = slide.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame
tf.word_wrap = True
tf.vertical_anchor = MSO_ANCHOR.MIDDLE
p = tf.paragraphs[0]
p.alignment = align
run = p.add_run()
run.text = text
run.font.size = Pt(font_size)
run.font.color.rgb = font_rgb
run.font.bold = bold
def connector(slide, x1, y1, x2, y2, color=None, width_pt=1.5):
from pptx.enum.shapes import MSO_CONNECTOR
color = color or GREY_DARK
ln = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, x1, y1, x2, y2)
ln.line.color.rgb = color
ln.line.width = Pt(width_pt)
return ln
def yes_no_buttons(slide, yes_text, no_text,
btn_y, center_x,
yes_color, yes_border,
no_color, no_border,
gap=Inches(1.2)):
"""Draw two clickable buttons centered on the slide, returns (yes_shp, no_shp)."""
BW = Inches(2.2)
BH = Inches(0.65)
yes_x = center_x - gap/2 - BW
no_x = center_x + gap/2
yes_shp = shape_box(slide, yes_x, btn_y, BW, BH, yes_color, yes_border, border_pt=2)
txt_in_shape(yes_shp, yes_text, 13, WHITE, bold=True)
no_shp = shape_box(slide, no_x, btn_y, BW, BH, no_color, no_border, border_pt=2)
txt_in_shape(no_shp, no_text, 13, WHITE, bold=True)
return yes_shp, no_shp
def home_button(slide, home_slide_idx, prs_obj):
"""Small home button top-right."""
hx = SW - Inches(1.3)
hy = Inches(0.12)
hw = Inches(1.1)
hh = Inches(0.38)
h = shape_box(slide, hx, hy, hw, hh, NAVY, None)
txt_in_shape(h, "β Home", 9, WHITE, bold=True)
add_slide_hyperlink(h, home_slide_idx, prs_obj)
def back_button(slide, back_slide_idx, prs_obj):
bx = SW - Inches(2.6)
by = Inches(0.12)
bw = Inches(1.1)
bh = Inches(0.38)
b = shape_box(slide, bx, by, bw, bh, GREY_DARK, None)
txt_in_shape(b, "β Back", 9, WHITE, bold=True)
add_slide_hyperlink(b, back_slide_idx, prs_obj)
def progress_bar(slide, current, total, label=""):
"""Thin progress strip at the bottom."""
BAR_H = Inches(0.18)
BAR_Y = SH - BAR_H
# Background track
track = shape_box(slide, 0, BAR_Y, SW, BAR_H, RGBColor(0xD5, 0xDB, 0xE1), None, radius=False)
# Fill
fill_w = int(SW * current / total)
if fill_w > 0:
fill = shape_box(slide, 0, BAR_Y, fill_w, BAR_H, TEAL, None, radius=False)
# Label
if label:
add_label(slide, label, SW - Inches(3.5), BAR_Y - Inches(0.02),
Inches(3.4), BAR_H, font_size=7, font_rgb=GREY_DARK, align=PP_ALIGN.RIGHT)
def diagnosis_card(slide, title, subtitle, details_lines, bg_color, border_color,
title_color, x=None, y=None, w=None, h=None):
"""Terminal diagnosis card."""
x = x or Inches(1.2)
y = y or Inches(1.5)
w = w or Inches(10.9)
h = h or Inches(4.5)
card = shape_box(slide, x, y, w, h, bg_color, border_color, border_pt=2.5)
tf = card.text_frame
tf.word_wrap = True
tf.vertical_anchor = MSO_ANCHOR.TOP
# margin
tf.margin_left = Inches(0.25)
tf.margin_top = Inches(0.18)
tf.margin_right = Inches(0.25)
p0 = tf.paragraphs[0]
p0.alignment = PP_ALIGN.LEFT
r0 = p0.add_run()
r0.text = title
r0.font.size = Pt(20)
r0.font.bold = True
r0.font.color.rgb = title_color
p1 = tf.add_paragraph()
p1.alignment = PP_ALIGN.LEFT
r1 = p1.add_run()
r1.text = subtitle
r1.font.size = Pt(12)
r1.font.italic = True
r1.font.color.rgb = GREY_DARK
p_blank = tf.add_paragraph()
p_blank.add_run().text = ""
for line in details_lines:
px = tf.add_paragraph()
px.alignment = PP_ALIGN.LEFT
rx = px.add_run()
rx.text = line
rx.font.size = Pt(10.5)
rx.font.color.rgb = GREY_DARK
return card
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SLIDE DEFINITIONS
# We build all slides first (so indices are fixed), then wire hyperlinks.
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Slide index map (0-based)
IDX = {
"cover": 0,
"q1_heard": 1, # Q1: Did you hear a murmur?
"q2_timing": 2, # Q2: Systolic or Diastolic?
"q2b_continuous": 3, # Branch: Continuous?
"q3_sys_timing": 4, # Q3: Systolic β early/mid/late/holo?
"q4_sys_holo": 5, # Q4: Holosystolic location?
"q4b_sys_ejection": 6, # Q4b: Ejection murmur β AS or PS?
"q5_dias_type": 7, # Q5: Diastolic β early or mid/late?
"q6_location": 8, # Q6: Location β right or left?
# Diagnosis slides
"dx_innocent": 9,
"dx_as": 10,
"dx_ps": 11,
"dx_mr": 12,
"dx_tr": 13,
"dx_vsd": 14,
"dx_hcm": 15,
"dx_ar": 16,
"dx_ms": 17,
"dx_pda": 18,
"dx_normal": 19,
"summary": 20,
}
# Pre-create all slides
slides = {}
for k in IDX:
slides[k] = prs.slides.add_slide(BLANK)
CX = SW / 2 # center X
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SLIDE 0 β COVER
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
s = slides["cover"]
bg(s, NAVY)
# Top accent bar
bar = shape_box(s, 0, 0, SW, Inches(0.08), TEAL, None, radius=False)
# Title box
t_box = shape_box(s, Inches(1.2), Inches(1.3), Inches(10.9), Inches(2.1), TEAL, None)
txt_in_shape(t_box, "Heart Murmur", 40, WHITE, bold=True,
second_line="Interactive Diagnostic Flowchart",
second_size=18, second_rgb=RGBColor(0xB0,0xD8,0xE8))
# Subtitle card
sub = shape_box(s, Inches(2.5), Inches(3.7), Inches(8.3), Inches(1.1),
RGBColor(0x24,0x3D,0x60), None)
txt_in_shape(sub,
"A step-by-step auscultation guide | Click any button to begin",
13, RGBColor(0xB0,0xD8,0xE8))
# Start button
start = shape_box(s, CX - Inches(1.5), Inches(5.2), Inches(3), Inches(0.75), GREEN, None)
txt_in_shape(start, "βΆ Start Diagnosis", 16, WHITE, bold=True)
add_slide_hyperlink(start, IDX["q1_heard"], prs)
# Source label
add_label(s, "Sources: Harrison's 22E Β· Guyton & Hall Β· Fuster & Hurst 15E | Orris Medical AI Β· July 2026",
Inches(1), Inches(6.8), Inches(11.3), Inches(0.4),
font_size=8, font_rgb=RGBColor(0x70,0x88,0xA8))
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SHARED QUESTION SLIDE BUILDER
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def question_slide(slide, step_num, total_steps, question_text, sub_text,
icon="π©Ί", header_bg=TEAL, question_bg=SKY, question_border=TEAL):
bg(slide, GREY_LITE)
# Header strip
hdr = shape_box(slide, 0, 0, SW, Inches(0.82), header_bg, None, radius=False)
txt_in_shape(hdr, f" {icon} HEART MURMUR DIAGNOSTIC FLOWCHART β STEP {step_num} of {total_steps}",
12, WHITE, bold=True, align=PP_ALIGN.LEFT)
# Question box
qbox = shape_box(slide, Inches(0.8), Inches(1.1), Inches(11.7), Inches(1.8),
question_bg, question_border, border_pt=2)
txt_in_shape(qbox, question_text, 20, NAVY, bold=True,
second_line=sub_text, second_size=11,
second_rgb=GREY_DARK)
progress_bar(slide, step_num, total_steps, f"Step {step_num} / {total_steps}")
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Q1 β Did you hear a murmur at all?
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
s = slides["q1_heard"]
question_slide(s, 1, 8,
"Did you hear an extra sound (murmur) between the normal heart sounds?",
"Place stethoscope at all 5 areas: Aortic β Pulmonary β Erb's Point β Tricuspid β Mitral/Apex",
icon="π")
home_button(s, IDX["cover"], prs)
# Info boxes
info1 = shape_box(s, Inches(0.8), Inches(3.2), Inches(5.6), Inches(2.1), BLUE_L, BLUE, border_pt=1.5)
txt_in_shape(info1,
"Normal heart sounds only:\n'lub... dub... lub... dub...'\n(S1 then S2, nothing extra)",
11, GREY_DARK, italic=True)
info2 = shape_box(s, Inches(6.9), Inches(3.2), Inches(5.6), Inches(2.1), AMBER_L, AMBER, border_pt=1.5)
txt_in_shape(info2,
"Abnormal β you hear something extra:\na whoosh, swish, or hiss between S1 and S2\n(or between S2 and the next S1)",
11, GREY_DARK, italic=True)
add_label(s, "No β Normal Heart Sounds Only", Inches(0.8), Inches(5.5), Inches(5.6), Inches(0.35),
font_size=11, font_rgb=GREY_DARK, bold=True, align=PP_ALIGN.CENTER)
add_label(s, "Yes β Extra Sound Heard", Inches(6.9), Inches(5.5), Inches(5.6), Inches(0.35),
font_size=11, font_rgb=GREY_DARK, bold=True, align=PP_ALIGN.CENTER)
no_btn = shape_box(s, Inches(1.5), Inches(6.05), Inches(4.2), Inches(0.65), GREY_DARK, None)
txt_in_shape(no_btn, "β No Murmur Detected", 13, WHITE, bold=True)
add_slide_hyperlink(no_btn, IDX["dx_normal"], prs)
yes_btn = shape_box(s, Inches(7.6), Inches(6.05), Inches(4.2), Inches(0.65), TEAL, None)
txt_in_shape(yes_btn, "β Yes β I Hear a Murmur", 13, WHITE, bold=True)
add_slide_hyperlink(yes_btn, IDX["q2_timing"], prs)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Q2 β Timing: Systolic, Diastolic, or Continuous?
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
s = slides["q2_timing"]
question_slide(s, 2, 8,
"When during the heartbeat do you hear the murmur?",
"Palpate the carotid pulse simultaneously β it rises with S1. The gap between S1βS2 is systole; after S2 is diastole.",
icon="β±οΈ")
home_button(s, IDX["cover"], prs)
back_button(s, IDX["q1_heard"], prs)
# Three option boxes
opts = [
("SYSTOLIC", "Between S1 (lub) and S2 (dub)\nDuring the squeeze / ejection",
RED_L, RED, IDX["q3_sys_timing"]),
("DIASTOLIC", "Between S2 (dub) and the next S1\nDuring relaxation / filling",
BLUE_L, BLUE, IDX["q5_dias_type"]),
("CONTINUOUS", "Heard through BOTH systole\nAND diastole without pause",
GREEN_L, GREEN, IDX["q2b_continuous"]),
]
ox = [Inches(0.55), Inches(4.75), Inches(8.95)]
for i, (title, body, b_col, b_bdr, target) in enumerate(opts):
card = shape_box(s, ox[i], Inches(3.1), Inches(3.8), Inches(2.5), b_col, b_bdr, border_pt=2)
tf = card.text_frame
tf.word_wrap = True
tf.vertical_anchor = MSO_ANCHOR.TOP
tf.margin_left = Inches(0.15); tf.margin_top = Inches(0.15)
p0 = tf.paragraphs[0]; p0.alignment = PP_ALIGN.CENTER
r0 = p0.add_run(); r0.text = title
r0.font.size = Pt(15); r0.font.bold = True; r0.font.color.rgb = b_bdr
p1 = tf.add_paragraph(); p1.alignment = PP_ALIGN.CENTER
r1 = p1.add_run(); r1.text = body
r1.font.size = Pt(10); r1.font.color.rgb = GREY_DARK
btn = shape_box(s, ox[i] + Inches(0.4), Inches(5.8), Inches(3.0), Inches(0.62), b_bdr, None)
txt_in_shape(btn, f"Select: {title}", 12, WHITE, bold=True)
add_slide_hyperlink(btn, target, prs)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Q2b β Continuous murmur
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
s = slides["q2b_continuous"]
question_slide(s, 2, 8,
"Continuous murmur β is it loudest around S2 (the 'dub')?",
"A continuous murmur that peaks around S2 with a 'machinery' quality strongly suggests PDA.",
icon="π", header_bg=GREEN, question_bg=GREEN_L, question_border=GREEN)
home_button(s, IDX["cover"], prs)
back_button(s, IDX["q2_timing"], prs)
info_box = shape_box(s, Inches(1.5), Inches(3.2), Inches(10.3), Inches(2.3), GREEN_L, GREEN, border_pt=1.5)
txt_in_shape(info_box,
"Patent Ductus Arteriosus (PDA) β Machinery Murmur\n\n"
"Location: Left infraclavicular / upper left sternal border\n"
"Quality: Harsh, 'machinery-like', waxes and wanes with each beat\n"
"Peaks: Louder in systole, quieter in diastole β but never silent",
11, GREY_DARK)
yes_btn = shape_box(s, Inches(3.5), Inches(5.7), Inches(3.0), Inches(0.65), GREEN, None)
txt_in_shape(yes_btn, "Yes β Likely PDA", 13, WHITE, bold=True)
add_slide_hyperlink(yes_btn, IDX["dx_pda"], prs)
no_btn = shape_box(s, Inches(7.0), Inches(5.7), Inches(3.0), Inches(0.65), GREY_DARK, None)
txt_in_shape(no_btn, "Unsure β Review Timing", 13, WHITE, bold=True)
add_slide_hyperlink(no_btn, IDX["q2_timing"], prs)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Q3 β Systolic: Holosystolic or Ejection (mid-systolic)?
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
s = slides["q3_sys_timing"]
question_slide(s, 3, 8,
"Is the systolic murmur HOLOSYSTOLIC (fills all of systole, plateau shape)?",
"Holosystolic = murmur starts with S1 and ends at S2 with equal loudness throughout (plateau). "
"Ejection = starts after S1, crescendo-decrescendo (diamond) shape, ends before S2.",
icon="π", header_bg=RED, question_bg=RED_L, question_border=RED)
home_button(s, IDX["cover"], prs)
back_button(s, IDX["q2_timing"], prs)
# Visual shape helpers
def timing_diagram_label(slide, x, y, label, color):
lbox = shape_box(slide, x, y, Inches(5.0), Inches(0.38), color, None)
txt_in_shape(lbox, label, 10, WHITE, bold=True)
timing_diagram_label(s, Inches(1.5), Inches(3.2),
"HOLOSYSTOLIC: S1 ββββββββββββββββββββββββ S2 (plateau β same loudness throughout)", RED)
timing_diagram_label(s, Inches(1.5), Inches(3.8),
"EJECTION: S1 βββ
ββ
ββ S2 (diamond shape β louder in middle)", AMBER)
add_label(s, "Examples of holosystolic: MR, TR, VSD", Inches(1.5), Inches(4.5), Inches(5.0), Inches(0.35),
font_size=10, font_rgb=RED)
add_label(s, "Examples of ejection: AS, PS, HCM, Innocent murmur", Inches(1.5), Inches(4.9), Inches(7.0), Inches(0.35),
font_size=10, font_rgb=AMBER)
yes_btn = shape_box(s, Inches(2.5), Inches(5.7), Inches(3.5), Inches(0.65), RED, None)
txt_in_shape(yes_btn, "Yes β Holosystolic (plateau) β", 13, WHITE, bold=True)
add_slide_hyperlink(yes_btn, IDX["q4_sys_holo"], prs)
no_btn = shape_box(s, Inches(7.3), Inches(5.7), Inches(3.5), Inches(0.65), AMBER, None)
txt_in_shape(no_btn, "No β Ejection (diamond shape) β", 13, WHITE, bold=True)
add_slide_hyperlink(no_btn, IDX["q4b_sys_ejection"], prs)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Q4 β Holosystolic: where is it loudest?
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
s = slides["q4_sys_holo"]
question_slide(s, 4, 8,
"Holosystolic murmur β where is it loudest?",
"Auscultate all areas and identify the site of maximal intensity.",
icon="π", header_bg=RED, question_bg=RED_L, question_border=RED)
home_button(s, IDX["cover"], prs)
back_button(s, IDX["q3_sys_timing"], prs)
locations = [
("Apex\n(5th ICS MCL)", "Blowing quality,\nradiates to axilla", PURPLE_L, PURPLE, IDX["dx_mr"]),
("LLSB\n(Lower L Sternal)", "Blowing, louder with\ninspiration", AMBER_L, AMBER, IDX["dx_tr"]),
("Lower LSB /\nBetween apex & LLSB", "Harsh, radiates all over,\nno clear radiation to neck", BLUE_L, BLUE, IDX["dx_vsd"]),
]
lx = [Inches(0.55), Inches(4.75), Inches(8.95)]
for i, (loc, detail, b_col, b_bdr, target) in enumerate(locations):
c = shape_box(s, lx[i], Inches(3.1), Inches(3.8), Inches(2.2), b_col, b_bdr, border_pt=2)
tf = c.text_frame; tf.word_wrap = True; tf.vertical_anchor = MSO_ANCHOR.MIDDLE
tf.margin_left = Inches(0.12); tf.margin_top = Inches(0.12)
p0 = tf.paragraphs[0]; p0.alignment = PP_ALIGN.CENTER
r0 = p0.add_run(); r0.text = loc
r0.font.size = Pt(13); r0.font.bold = True; r0.font.color.rgb = b_bdr
p1 = tf.add_paragraph(); p1.alignment = PP_ALIGN.CENTER
r1 = p1.add_run(); r1.text = detail
r1.font.size = Pt(10); r1.font.color.rgb = GREY_DARK
b = shape_box(s, lx[i] + Inches(0.4), Inches(5.5), Inches(3.0), Inches(0.6), b_bdr, None)
txt_in_shape(b, "β Select", 12, WHITE, bold=True)
add_slide_hyperlink(b, target, prs)
# Labels below cards
add_label(s, "β Mitral Regurgitation (MR)", Inches(0.55), Inches(6.2), Inches(3.8), Inches(0.35),
font_size=10, font_rgb=PURPLE, bold=True, align=PP_ALIGN.CENTER)
add_label(s, "β Tricuspid Regurgitation (TR)", Inches(4.75), Inches(6.2), Inches(3.8), Inches(0.35),
font_size=10, font_rgb=AMBER, bold=True, align=PP_ALIGN.CENTER)
add_label(s, "β VSD / HCM", Inches(8.95), Inches(6.2), Inches(3.8), Inches(0.35),
font_size=10, font_rgb=BLUE, bold=True, align=PP_ALIGN.CENTER)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Q4b β Ejection murmur: AS or PS?
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
s = slides["q4b_sys_ejection"]
question_slide(s, 4, 8,
"Ejection (crescendo-decrescendo) murmur β where is it loudest?",
"Ejection murmurs are loudest at the base of the heart. Location and radiation help distinguish AS from PS.",
icon="π", header_bg=AMBER, question_bg=AMBER_L, question_border=AMBER)
home_button(s, IDX["cover"], prs)
back_button(s, IDX["q3_sys_timing"], prs)
opts4b = [
("Right Upper Sternal\nBorder (RUSB)\n2nd R ICS",
"Radiates to neck/carotids\nHarsh, rough quality\nβ Aortic Stenosis (AS)",
RED_L, RED, IDX["dx_as"]),
("Left Upper Sternal\nBorder (LUSB)\n2nd L ICS",
"Preceded by ejection click\nRadiates to left shoulder\nβ Pulmonary Stenosis (PS)",
BLUE_L, BLUE, IDX["dx_ps"]),
("LLSB / Apex\nChanges with Valsalva",
"β with Valsalva/standing\nβ with squatting\nβ HCM / HOCM",
PURPLE_L, PURPLE, IDX["dx_hcm"]),
("Soft, changes\nwith posture,\nasymptomatic child",
"No radiation, no click\nNo structural findings\nβ Innocent Murmur",
GREEN_L, GREEN, IDX["dx_innocent"]),
]
ox4b = [Inches(0.3), Inches(3.6), Inches(6.9), Inches(10.1)]
for i, (loc, detail, b_col, b_bdr, target) in enumerate(opts4b):
c = shape_box(s, ox4b[i], Inches(3.05), Inches(3.0), Inches(2.3), b_col, b_bdr, border_pt=2)
tf = c.text_frame; tf.word_wrap = True; tf.vertical_anchor = MSO_ANCHOR.TOP
tf.margin_left = Inches(0.1); tf.margin_top = Inches(0.12)
p0 = tf.paragraphs[0]; p0.alignment = PP_ALIGN.CENTER
r0 = p0.add_run(); r0.text = loc
r0.font.size = Pt(11); r0.font.bold = True; r0.font.color.rgb = b_bdr
p1 = tf.add_paragraph(); p1.alignment = PP_ALIGN.LEFT
r1 = p1.add_run(); r1.text = "\n" + detail
r1.font.size = Pt(9.5); r1.font.color.rgb = GREY_DARK
b = shape_box(s, ox4b[i] + Inches(0.25), Inches(5.5), Inches(2.5), Inches(0.55), b_bdr, None)
txt_in_shape(b, "Select β", 12, WHITE, bold=True)
add_slide_hyperlink(b, target, prs)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Q5 β Diastolic: Early or Mid/Late?
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
s = slides["q5_dias_type"]
question_slide(s, 5, 8,
"Diastolic murmur β when in diastole does it occur?",
"Remember: ANY diastolic murmur is pathological and always warrants echocardiography.",
icon="π΅", header_bg=BLUE, question_bg=BLUE_L, question_border=BLUE)
home_button(s, IDX["cover"], prs)
back_button(s, IDX["q2_timing"], prs)
# Warning banner
warn = shape_box(s, Inches(1.0), Inches(3.1), Inches(11.3), Inches(0.55), RED_L, RED, border_pt=1.5)
txt_in_shape(warn, "β ALL DIASTOLIC MURMURS ARE PATHOLOGICAL β Echocardiography is always indicated", 11, RED, bold=True)
opts5 = [
("EARLY DIASTOLIC\n(starts immediately after S2)",
"Decrescendo, high-pitched\nBlowing quality\nBest heard: LSB leaning forward\nβ Aortic or Pulmonary Regurgitation",
RED_L, RED, IDX["q6_location"]),
("MID / LATE DIASTOLIC\n(starts after a gap post-S2)",
"Low-pitched rumble (use bell)\nApex, often with opening snap (OS)\nLoud S1 clue\nβ Mitral or Tricuspid Stenosis",
BLUE_L, BLUE, IDX["dx_ms"]),
]
ox5 = [Inches(0.8), Inches(7.1)]
for i, (title, detail, b_col, b_bdr, target) in enumerate(opts5):
c = shape_box(s, ox5[i], Inches(3.9), Inches(5.7), Inches(2.3), b_col, b_bdr, border_pt=2)
tf = c.text_frame; tf.word_wrap = True; tf.vertical_anchor = MSO_ANCHOR.TOP
tf.margin_left = Inches(0.15); tf.margin_top = Inches(0.15)
p0 = tf.paragraphs[0]; p0.alignment = PP_ALIGN.CENTER
r0 = p0.add_run(); r0.text = title
r0.font.size = Pt(12); r0.font.bold = True; r0.font.color.rgb = b_bdr
p1 = tf.add_paragraph(); p1.alignment = PP_ALIGN.LEFT
r1 = p1.add_run(); r1.text = "\n" + detail
r1.font.size = Pt(10.5); r1.font.color.rgb = GREY_DARK
b = shape_box(s, ox5[i] + Inches(0.85), Inches(5.9), Inches(4.0), Inches(0.6), b_bdr, None)
txt_in_shape(b, "Select β", 12, WHITE, bold=True)
add_slide_hyperlink(b, target, prs)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Q6 β Early diastolic: right or left side?
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
s = slides["q6_location"]
question_slide(s, 6, 8,
"Early diastolic decrescendo murmur β does it increase with INSPIRATION?",
"Right-sided murmurs increase with inspiration (Carvallo's sign) due to increased venous return. Left-sided murmurs do not.",
icon="π¨", header_bg=BLUE, question_bg=BLUE_L, question_border=BLUE)
home_button(s, IDX["cover"], prs)
back_button(s, IDX["q5_dias_type"], prs)
info = shape_box(s, Inches(1.0), Inches(3.2), Inches(11.3), Inches(1.8), BLUE_L, BLUE, border_pt=1.5)
txt_in_shape(info,
"Aortic Regurgitation (AR): Left-sided. Heard best at LEFT sternal border, louder on EXPIRATION. "
"High-pitched, blowing, decrescendo. Wide pulse pressure ('water hammer' pulse, Corrigan's pulse).\n"
"Pulmonary Regurgitation (PR): Right-sided. Louder on INSPIRATION. "
"Graham Steell murmur β seen in pulmonary hypertension.",
10, GREY_DARK)
yes_btn = shape_box(s, Inches(2.0), Inches(5.5), Inches(4.0), Inches(0.7), AMBER, None)
txt_in_shape(yes_btn, "Yes β Louder on Inspiration\nβ Pulmonary Regurgitation", 12, WHITE, bold=True)
add_slide_hyperlink(yes_btn, IDX["dx_ar"], prs) # Note: using AR slide for both, labelled
no_btn = shape_box(s, Inches(7.3), Inches(5.5), Inches(4.0), Inches(0.7), BLUE, None)
txt_in_shape(no_btn, "No β Louder on Expiration\nβ Aortic Regurgitation (AR)", 12, WHITE, bold=True)
add_slide_hyperlink(no_btn, IDX["dx_ar"], prs)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# DIAGNOSIS SLIDES β helper
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def dx_slide(slide_key, step, title, abbrev, timing, location, radiation,
quality, config, grade, key_clue, causes, treatment_note,
bg_c, bd_c, title_c, icon="π₯"):
s = slides[slide_key]
bg(s, GREY_LITE)
# Header
hdr = shape_box(s, 0, 0, SW, Inches(0.82), bd_c, None, radius=False)
txt_in_shape(hdr, f" {icon} DIAGNOSIS β Step {step} of 8", 12, WHITE, bold=True, align=PP_ALIGN.LEFT)
home_button(s, IDX["cover"], prs)
# Title banner
tb2 = shape_box(s, Inches(0.4), Inches(0.95), Inches(12.5), Inches(0.8), bd_c, None)
txt_in_shape(tb2, f"{title} ({abbrev})", 18, WHITE, bold=True)
# Main info card
card = shape_box(s, Inches(0.4), Inches(1.85), Inches(12.5), Inches(4.25), bg_c, bd_c, border_pt=1.5)
tf = card.text_frame; tf.word_wrap = True; tf.vertical_anchor = MSO_ANCHOR.TOP
tf.margin_left = Inches(0.2); tf.margin_top = Inches(0.15); tf.margin_right = Inches(0.2)
lines = [
("β± Timing:", timing),
("π Location:", location),
("β‘ Radiation:", radiation),
("π Quality:", quality),
("π Configuration:", config),
("π’ Grade:", grade),
("π Key Clue:", key_clue),
("π¬ Common Causes:", causes),
("π Management Note:", treatment_note),
]
first = True
for label, value in lines:
p = tf.paragraphs[0] if first else tf.add_paragraph()
first = False
p.alignment = PP_ALIGN.LEFT
r_l = p.add_run(); r_l.text = f"{label} "
r_l.font.size = Pt(10.5); r_l.font.bold = True; r_l.font.color.rgb = bd_c
r_v = p.add_run(); r_v.text = value
r_v.font.size = Pt(10.5); r_v.font.color.rgb = GREY_DARK
# Summary row button strip
summary_btn = shape_box(s, Inches(5.0), Inches(6.3), Inches(3.3), Inches(0.65), TEAL, None)
txt_in_shape(summary_btn, "π See All Diagnoses", 12, WHITE, bold=True)
add_slide_hyperlink(summary_btn, IDX["summary"], prs)
restart_btn = shape_box(s, Inches(9.0), Inches(6.3), Inches(3.3), Inches(0.65), NAVY, None)
txt_in_shape(restart_btn, "π Restart Flowchart", 12, WHITE, bold=True)
add_slide_hyperlink(restart_btn, IDX["q1_heard"], prs)
progress_bar(s, step, 8)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Build all diagnosis slides
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
dx_slide("dx_innocent", 8,
"Innocent (Functional) Murmur", "Innocent",
"Systolic only (never diastolic)",
"Variable β often lower left sternal border or apex",
"None",
"Soft, blowing or vibratory (Still's murmur)",
"Crescendo-decrescendo (ejection type)",
"Grade I β II / VI",
"Changes with posture; disappears with Valsalva; no thrill; asymptomatic child",
"Fever, anemia, pregnancy, high cardiac output states; structurally normal heart",
"No treatment needed. Reassure patient. No echocardiography required if asymptomatic + Grade IβII.",
GREEN_L, GREEN, GREEN, icon="β
")
dx_slide("dx_as", 8,
"Aortic Stenosis", "AS",
"Systolic (ejection / crescendo-decrescendo)",
"Right upper sternal border (RUSB), 2nd R ICS",
"Neck / carotids (Gallavardin: musical quality at apex)",
"Harsh, rough, coarse",
"Crescendo-decrescendo (diamond shape)",
"Grade II β V / VI (thrill at grade IV+)",
"Slow-rising carotid pulse (pulsus parvus et tardus); S4; delayed S2",
"Calcific degeneration (elderly, >65), Bicuspid aortic valve (young adults), Rheumatic fever",
"Severe AS: Aortic valve replacement (SAVR) or TAVR. Symptoms = surgical urgency. Classic triad: Angina, Syncope, Dyspnoea.",
RED_L, RED, RED, icon="π΄")
dx_slide("dx_ps", 8,
"Pulmonary Stenosis", "PS",
"Systolic (ejection / crescendo-decrescendo)",
"Left upper sternal border (LUSB), 2nd L ICS",
"Left shoulder and back; right ventricular area",
"Harsh, blowing",
"Crescendo-decrescendo (spills past A2 in severe PS)",
"Grade II β IV / VI",
"Ejection click that disappears on inspiration; widely split S2; RV heave",
"Congenital (most common), Carcinoid syndrome (right-sided valvular disease)",
"Mild: observe. Severe: Balloon pulmonary valvuloplasty (procedure of choice in congenital PS).",
BLUE_L, BLUE, BLUE, icon="π΅")
dx_slide("dx_mr", 8,
"Mitral Regurgitation", "MR",
"Holosystolic (pansystolic) β starts with S1, ends at S2",
"Apex (mitral area) β 5th L ICS midclavicular line",
"Left axilla / back (toward armpit)",
"Blowing, high-pitched",
"Plateau (uniform loudness from S1 to S2)",
"Grade II β IV / VI (can be grade V in acute severe MR)",
"Soft S1 (valve cannot close properly); S3 (volume overload); displaced PMI",
"Mitral valve prolapse (MVP), Rheumatic fever, IE, Papillary muscle rupture (post-MI), Dilated cardiomyopathy",
"Afterload reduction (ACE inhibitors). Severe/symptomatic: Mitral valve repair (preferred) or replacement.",
PURPLE_L, PURPLE, PURPLE, icon="π£")
dx_slide("dx_tr", 8,
"Tricuspid Regurgitation", "TR",
"Holosystolic β increases with inspiration (Carvallo's sign)",
"Left lower sternal border (LLSB), 4thβ5th L ICS",
"None; increases in intensity with deep inspiration",
"Blowing, soft",
"Plateau",
"Grade I β III / VI (often soft)",
"Pulsatile liver; raised JVP with prominent 'v' wave; peripheral oedema",
"RV dilation (secondary/functional β most common), IE in IV drug users, Carcinoid, Rheumatic fever",
"Treat underlying cause (RV failure, diuretics). Severe: Annuloplasty or valve replacement.",
AMBER_L, AMBER, AMBER, icon="π ")
dx_slide("dx_vsd", 8,
"Ventricular Septal Defect", "VSD",
"Holosystolic β present from birth in congenital cases",
"Lower left sternal border (LLSB) / parasternal area",
"Right side of sternum (right parasternal)",
"Harsh, loud",
"Plateau",
"Grade III β V/VI (louder murmur = smaller defect: Maladie de Roger)",
"'Restrictive' VSD has louder murmur; thrill common; large VSDs may be surprisingly quiet",
"Congenital (most common), Post-MI ventricular septal rupture (surgical emergency)",
"Small VSD: may close spontaneously in childhood. Large: surgical or catheter-based closure. Post-MI rupture: emergency surgery.",
BLUE_L, BLUE, BLUE, icon="π΅")
dx_slide("dx_hcm", 8,
"Hypertrophic Cardiomyopathy (Obstructive)", "HCM/HOCM",
"Systolic ejection β crescendo-decrescendo",
"LLSB / between apex and LLSB",
"No radiation to neck (unlike AS)",
"Harsh",
"Crescendo-decrescendo",
"Grade II β IV / VI",
"β with Valsalva and standing; β with squatting and handgrip β opposite of AS",
"Genetic (autosomal dominant β sarcomere mutations: MYH7, MYBPC3), most common cause of sudden cardiac death in young athletes",
"Beta-blockers or verapamil (1st line). ICD if high-risk SCD features. Septal myectomy or alcohol septal ablation in refractory cases.",
PURPLE_L, PURPLE, PURPLE, icon="π£")
dx_slide("dx_ar", 8,
"Aortic Regurgitation", "AR",
"Early diastolic β immediately after S2",
"Left sternal border (3rdβ4th L ICS = Erb's point); sit forward, breath held in expiration",
"Apex β Austin Flint murmur (functional MS rumble from regurgitant jet hitting mitral leaflet)",
"High-pitched, blowing, soft",
"Decrescendo (fades as pressure gradient equalizes)",
"Grade I β III / VI (can be missed)",
"Wide pulse pressure; 'water hammer' (Corrigan's) pulse; head nodding (de Musset's sign); capillary pulsations (Quincke's sign)",
"Aortic root dilation (Marfan syndrome, hypertension), Infective endocarditis, Bicuspid aortic valve, Rheumatic fever",
"Chronic: ACE inhibitors for afterload reduction. Severe + symptomatic or LV dysfunction: Aortic valve replacement.",
RED_L, RED, RED, icon="π΄")
dx_slide("dx_ms", 8,
"Mitral Stenosis", "MS",
"Mid-to-late diastolic β begins after opening snap (OS), low rumble into diastole",
"Apex β LEFT lateral decubitus position with bell of stethoscope",
"Does not radiate",
"Low-pitched rumble (must use bell, not diaphragm)",
"Rumble with presystolic accentuation (in sinus rhythm) β crescendo toward S1",
"Grade I β III / VI (easily missed; requires bell + patient turned left)",
"Loud S1; opening snap (OS) shortly after S2 β shorter OS-S2 interval = more severe MS; RV heave; AF common",
"Rheumatic fever (most common worldwide β Group A Streptococcus β antibody cross-reaction with valve)",
"Percutaneous mitral balloon valvotomy (PMBV) β if suitable anatomy. Anticoagulation if AF. Surgical replacement if not suitable.",
BLUE_L, BLUE, BLUE, icon="π΅")
dx_slide("dx_pda", 8,
"Patent Ductus Arteriosus", "PDA",
"Continuous β through both systole AND diastole",
"Left infraclavicular area / 2nd L ICS (LUSB)",
"Left shoulder; radiates toward clavicle",
"Harsh, machinery-like",
"Waxes in systole, wanes in diastole; peaks around S2",
"Grade II β IV / VI",
"Machinery murmur β distinctive continuous character; bounding pulse",
"Prematurity, congenital rubella syndrome, maternal indomethacin deficiency; failure of normal postnatal closure",
"Premature infants: Indomethacin or Ibuprofen (inhibit prostaglandin, promote closure). Term infants/children: Catheter occlusion device (1st choice) or surgical ligation.",
GREEN_L, GREEN, GREEN, icon="π’")
dx_slide("dx_normal", 1,
"No Murmur Detected β Normal Auscultation", "Normal",
"N/A β no murmur heard",
"N/A",
"N/A",
"Normal: lub... dub... (S1 and S2 only)",
"N/A",
"N/A",
"Continue routine assessment. If clinical suspicion remains high (e.g. symptoms, signs), consider echocardiography even without audible murmur.",
"Conditions with 'silent' presentations include severe MS (low flow), severe AR, or early HCM",
"If low clinical suspicion: no further cardiac workup needed. If high suspicion despite no murmur: request echo.",
GREY_LITE, GREY_DARK, GREY_DARK, icon="β
")
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SUMMARY SLIDE
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
s = slides["summary"]
bg(s, NAVY)
hdr = shape_box(s, 0, 0, SW, Inches(0.82), TEAL, None, radius=False)
txt_in_shape(hdr, " π MURMUR QUICK NAVIGATION β Click any diagnosis to open its detail slide", 12, WHITE, bold=True, align=PP_ALIGN.LEFT)
home_button(s, IDX["cover"], prs)
title_b = shape_box(s, Inches(1.0), Inches(0.9), Inches(11.3), Inches(0.5), NAVY, None)
txt_in_shape(title_b, "Select a Diagnosis to Review", 15, RGBColor(0xB0,0xD8,0xE8), bold=True)
# Grid of diagnosis buttons
dx_buttons = [
("Aortic Stenosis (AS)", IDX["dx_as"], RED, "Systolic Β· Ejection Β· RUSB"),
("Mitral Regurgitation (MR)", IDX["dx_mr"], PURPLE, "Systolic Β· Holosystolic Β· Apex"),
("Pulmonary Stenosis (PS)", IDX["dx_ps"], BLUE, "Systolic Β· Ejection Β· LUSB"),
("Tricuspid Regurgitation (TR)",IDX["dx_tr"], AMBER, "Systolic Β· Holosystolic Β· LLSB"),
("VSD", IDX["dx_vsd"], BLUE, "Systolic Β· Holosystolic Β· LLSB"),
("HCM / HOCM", IDX["dx_hcm"], PURPLE, "Systolic Β· Ejection Β· βValsalva"),
("Aortic Regurgitation (AR)", IDX["dx_ar"], RED, "Diastolic Β· Early Β· Decrescendo"),
("Mitral Stenosis (MS)", IDX["dx_ms"], BLUE, "Diastolic Β· Mid/Late Β· Rumble"),
("PDA", IDX["dx_pda"], GREEN, "Continuous Β· Machinery"),
("Innocent Murmur", IDX["dx_innocent"], GREEN, "Systolic Β· Soft Β· No radiation"),
("Normal β No Murmur", IDX["dx_normal"], GREY_DARK, "No murmur detected"),
("Restart Flowchart", IDX["q1_heard"], TEAL, "Go back to Step 1"),
]
cols = 3
btn_w = Inches(4.0)
btn_h = Inches(0.68)
gap_x = Inches(0.22)
gap_y = Inches(0.18)
start_x = Inches(0.55)
start_y = Inches(1.55)
for i, (label, target, color, sub) in enumerate(dx_buttons):
row = i // cols
col = i % cols
x = start_x + col * (btn_w + gap_x)
y = start_y + row * (btn_h + gap_y)
b = shape_box(s, x, y, btn_w, btn_h, color, None)
tf = b.text_frame; tf.word_wrap = True; tf.vertical_anchor = MSO_ANCHOR.MIDDLE
tf.margin_left = Inches(0.12); tf.margin_top = Inches(0.05)
p0 = tf.paragraphs[0]; p0.alignment = PP_ALIGN.LEFT
r0 = p0.add_run(); r0.text = label
r0.font.size = Pt(11); r0.font.bold = True; r0.font.color.rgb = WHITE
p1 = tf.add_paragraph(); p1.alignment = PP_ALIGN.LEFT
r1 = p1.add_run(); r1.text = sub
r1.font.size = Pt(8.5); r1.font.color.rgb = RGBColor(0xCC,0xDD,0xEE)
add_slide_hyperlink(b, target, prs)
add_label(s, "Sources: Harrison's 22E Β· Guyton & Hall Β· Fuster & Hurst 15E | Orris Medical AI Β· July 2026",
Inches(1), Inches(7.1), Inches(11.3), Inches(0.3),
font_size=7.5, font_rgb=RGBColor(0x70,0x88,0xA8))
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SAVE
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
OUTPUT = "/tmp/workspace/murmur-reference/Heart_Murmur_Flowchart_Interactive.pptx"
buf = BytesIO()
prs.save(buf)
with open(OUTPUT, "wb") as f:
f.write(buf.getvalue())
print(f"Saved: {OUTPUT}")
import os
size = os.path.getsize(OUTPUT)
print(f"Size: {size/1024:.1f} KB | Slides: {len(prs.slides)}")
cd /tmp/workspace/murmur-reference && python build_flowchart.py 2>&1
Heart Murmur Flowchart Interactive
Presentation Β· PPTX
| Slide | Question | Branches to |
|---|---|---|
| Cover | β | "Start Diagnosis" button |
| Q1 | Did you hear a murmur? | No β Normal Β· Yes β Q2 |
| Q2 | Systolic / Diastolic / Continuous? | 3-way branch |
| Q2b | Continuous β loudest near S2? | β PDA or back |
| Q3 | Systolic β holosystolic or ejection? | 2-way branch |
| Q4 | Holosystolic β location? | β MR Β· TR Β· VSD |
| Q4b | Ejection β location? | β AS Β· PS Β· HCM Β· Innocent |
| Q5 | Diastolic β early or mid/late? | β AR/PR Β· MS |
| Q6 | Early diastolic β louder on inspiration? | β AR Β· PR |
Can u make a explanation video
Create a summary document to accompany the flowchart as handout
~/murmur-reference/build_handout.py
"""
Heart Murmur Diagnostic Flowchart β Companion Handout
Built with python-docx | Orris Medical AI
A clean, print-ready Word document designed to accompany the interactive PPTX.
"""
from docx import Document
from docx.shared import Pt, Inches, RGBColor, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
import copy
OUTPUT = "/tmp/workspace/murmur-reference/Heart_Murmur_Handout.docx"
# ββ Colour palette βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
NAVY = RGBColor(0x1A, 0x2B, 0x4A)
TEAL = RGBColor(0x0D, 0x73, 0x77)
TEAL_L = RGBColor(0xD0, 0xED, 0xEE)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
RED = RGBColor(0xC0, 0x39, 0x2B)
RED_L = RGBColor(0xFC, 0xE8, 0xE6)
BLUE = RGBColor(0x29, 0x80, 0xB9)
BLUE_L = RGBColor(0xE3, 0xF0, 0xFA)
GREEN = RGBColor(0x27, 0xAE, 0x60)
GREEN_L = RGBColor(0xE4, 0xF5, 0xE9)
AMBER = RGBColor(0xE6, 0x7E, 0x22)
AMBER_L = RGBColor(0xFD, 0xF2, 0xE4)
PURPLE = RGBColor(0x8E, 0x44, 0xAD)
PURPLE_L= RGBColor(0xF2, 0xEC, 0xF8)
GREY = RGBColor(0x44, 0x55, 0x66)
GREY_L = RGBColor(0xF2, 0xF4, 0xF6)
GREY_H = RGBColor(0xD5, 0xDB, 0xE1)
# ββ Low-level XML helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def set_cell_bg(cell, rgb: RGBColor):
hex_val = f"{rgb[0]:02X}{rgb[1]:02X}{rgb[2]:02X}"
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
shd = OxmlElement("w:shd")
shd.set(qn("w:val"), "clear")
shd.set(qn("w:color"), "auto")
shd.set(qn("w:fill"), hex_val)
tcPr.append(shd)
def set_cell_borders(cell, top=None, bottom=None, left=None, right=None):
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
tcBorders = OxmlElement("w:tcBorders")
for side, val in [("top", top), ("bottom", bottom), ("left", left), ("right", right)]:
if val:
b = OxmlElement(f"w:{side}")
b.set(qn("w:val"), val.get("val", "single"))
b.set(qn("w:sz"), str(val.get("sz", 4)))
b.set(qn("w:color"), val.get("color", "000000"))
tcBorders.append(b)
tcPr.append(tcBorders)
def set_para_spacing(para, before=0, after=0, line=None):
pPr = para._p.get_or_add_pPr()
spacing = OxmlElement("w:spacing")
spacing.set(qn("w:before"), str(before))
spacing.set(qn("w:after"), str(after))
if line:
spacing.set(qn("w:line"), str(line))
spacing.set(qn("w:lineRule"), "auto")
pPr.append(spacing)
def set_table_borders(table, color="D5DBE1", sz=4):
tbl = table._tbl
tblPr = tbl.find(qn("w:tblPr"))
if tblPr is None:
tblPr = OxmlElement("w:tblPr")
tbl.insert(0, tblPr)
tblBorders = OxmlElement("w:tblBorders")
for side in ["top", "left", "bottom", "right", "insideH", "insideV"]:
b = OxmlElement(f"w:{side}")
b.set(qn("w:val"), "single")
b.set(qn("w:sz"), str(sz))
b.set(qn("w:color"), color)
tblBorders.append(b)
tblPr.append(tblBorders)
def page_break(doc):
doc.add_page_break()
# ββ Document βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
doc = Document()
# Page setup β A4, narrow margins
section = doc.sections[0]
section.page_width = Cm(21.0)
section.page_height = Cm(29.7)
section.top_margin = Cm(1.8)
section.bottom_margin = Cm(1.8)
section.left_margin = Cm(1.8)
section.right_margin = Cm(1.8)
# Default style
normal = doc.styles["Normal"]
normal.font.name = "Calibri"
normal.font.size = Pt(10)
normal.font.color.rgb = GREY
# ββ Helper: styled heading ββββββββββββββββββββββββββββββββββββββββββββββββββββ
def add_heading(doc, text, level=1, color=NAVY, size=None, after=80):
sizes = {1: 18, 2: 13, 3: 11}
p = doc.add_paragraph()
set_para_spacing(p, before=120 if level == 1 else 80, after=after)
run = p.add_run(text)
run.font.name = "Calibri"
run.font.size = Pt(size or sizes.get(level, 11))
run.font.bold = True
run.font.color.rgb = color
if level == 1:
# Underline accent
run2 = p.add_run(" ")
run2.font.size = Pt(sizes[1])
return p
def add_body(doc, text, italic=False, color=None, size=10, before=20, after=20, bold=False):
p = doc.add_paragraph()
set_para_spacing(p, before=before, after=after)
run = p.add_run(text)
run.font.name = "Calibri"
run.font.size = Pt(size)
run.font.color.rgb = color or GREY
run.font.italic = italic
run.font.bold = bold
return p
def add_bullet(doc, text, indent=0, color=None, size=10, bold_prefix=None):
p = doc.add_paragraph(style="List Bullet")
set_para_spacing(p, before=10, after=10)
if bold_prefix:
rb = p.add_run(bold_prefix + " ")
rb.font.bold = True
rb.font.color.rgb = color or GREY
rb.font.size = Pt(size)
run = p.add_run(text)
run.font.name = "Calibri"
run.font.size = Pt(size)
run.font.color.rgb = color or GREY
return p
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# HEADER BANNER (table-based)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def banner(doc, line1, line2, bg=NAVY, fg=WHITE, sub_fg=None):
t = doc.add_table(rows=1, cols=1)
t.alignment = WD_TABLE_ALIGNMENT.CENTER
cell = t.cell(0, 0)
set_cell_bg(cell, bg)
cell.width = Cm(17.4)
p1 = cell.paragraphs[0]
p1.alignment = WD_ALIGN_PARAGRAPH.CENTER
set_para_spacing(p1, before=100, after=20)
r1 = p1.add_run(line1)
r1.font.name = "Calibri"
r1.font.size = Pt(20)
r1.font.bold = True
r1.font.color.rgb = fg
p2 = cell.add_paragraph()
p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
set_para_spacing(p2, before=0, after=100)
r2 = p2.add_run(line2)
r2.font.name = "Calibri"
r2.font.size = Pt(10)
r2.font.color.rgb = sub_fg or RGBColor(0xB0, 0xD8, 0xE8)
r2.font.italic = True
return t
banner(doc,
"π« Heart Murmur Diagnostic Flowchart",
"Companion Handout | Use with the Interactive PowerPoint Flowchart "
"| Sources: Harrison's 22E Β· Guyton & Hall Β· Fuster & Hurst 15E | Orris Medical AI")
doc.add_paragraph()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 1 β HOW TO USE THIS HANDOUT
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
add_heading(doc, "HOW TO USE THIS HANDOUT", level=1, color=TEAL)
add_body(doc,
"This document is a print-ready companion to the Interactive Heart Murmur Diagnostic Flowchart (PPTX). "
"Use it alongside the flowchart during auscultation practice or clinical revision. "
"Each section mirrors a step in the flowchart so you can follow along on paper.",
before=10, after=20)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 2 β THE DIAGNOSTIC PATHWAY (compact flowchart in text)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
add_heading(doc, "THE DIAGNOSTIC PATHWAY AT A GLANCE", level=1, color=TEAL)
steps = [
("Step 1", "Did you hear an extra sound?",
"No β Normal. Yes β proceed to Step 2."),
("Step 2", "When does it occur?",
"Systolic (S1βS2) Β· Diastolic (S2βnext S1) Β· Continuous (both phases) β proceed accordingly."),
("Step 3 (Systolic)", "Holosystolic or Ejection (diamond shape)?",
"Holosystolic (plateau) β Step 4A. Ejection (crescendo-decrescendo) β Step 4B."),
("Step 4A (Holosystolic)", "Where is it loudest?",
"Apex β MR. LLSB + louder on inspiration β TR. LLSB/parasternal β VSD."),
("Step 4B (Ejection)", "Where is it loudest?",
"RUSB + radiates to neck β AS. LUSB + ejection click β PS. "
"LLSB, β Valsalva β HCM. Soft, no radiation, asymptomatic child β Innocent."),
("Step 5 (Diastolic)", "Early or mid/late diastolic?",
"Early (right after S2) β Step 6. Mid/late + rumble β MS (or TS)."),
("Step 6 (Early diastolic)", "Louder on inspiration?",
"Yes (right-sided) β Pulmonary Regurgitation. No (left-sided) β Aortic Regurgitation (AR)."),
("Continuous", "Machinery murmur, peaks near S2?",
"Yes β Patent Ductus Arteriosus (PDA)."),
]
t_steps = doc.add_table(rows=len(steps)+1, cols=3)
t_steps.alignment = WD_TABLE_ALIGNMENT.CENTER
t_steps.style = "Table Grid"
set_table_borders(t_steps, color="B0C4D0", sz=4)
# Header row
headers = ["Step", "Question", "Decision / Outcome"]
for i, h in enumerate(headers):
cell = t_steps.cell(0, i)
set_cell_bg(cell, NAVY)
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
set_para_spacing(p, before=40, after=40)
r = p.add_run(h)
r.font.bold = True; r.font.color.rgb = WHITE; r.font.size = Pt(9.5); r.font.name = "Calibri"
# Data rows
col_widths = [Cm(2.6), Cm(5.5), Cm(9.3)]
for i, (step, q, outcome) in enumerate(steps):
row_idx = i + 1
bg_c = TEAL_L if i % 2 == 0 else WHITE
# Step
c0 = t_steps.cell(row_idx, 0)
set_cell_bg(c0, TEAL)
p = c0.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
set_para_spacing(p, before=40, after=40)
r = p.add_run(step); r.font.bold = True; r.font.color.rgb = WHITE
r.font.size = Pt(9); r.font.name = "Calibri"
# Question
c1 = t_steps.cell(row_idx, 1)
set_cell_bg(c1, bg_c)
p = c1.paragraphs[0]
set_para_spacing(p, before=40, after=40)
r = p.add_run(q); r.font.bold = True; r.font.color.rgb = NAVY
r.font.size = Pt(9); r.font.name = "Calibri"
# Outcome
c2 = t_steps.cell(row_idx, 2)
set_cell_bg(c2, bg_c)
p = c2.paragraphs[0]
set_para_spacing(p, before=40, after=40)
r = p.add_run(outcome); r.font.color.rgb = GREY
r.font.size = Pt(9); r.font.name = "Calibri"
# Set column widths
for row in t_steps.rows:
for i, cell in enumerate(row.cells):
cell.width = col_widths[i]
doc.add_paragraph()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PAGE BREAK + SECTION 3 β MURMUR CHARACTERISTICS TABLE
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
page_break(doc)
add_heading(doc, "MURMUR CHARACTERISTICS REFERENCE TABLE", level=1, color=TEAL)
add_body(doc,
"S = Systolic | D = Diastolic | C = Continuous | "
"RUSB = Right upper sternal border | LUSB = Left upper sternal border | "
"LLSB = Left lower sternal border | LSB = Left sternal border",
italic=True, size=8.5, before=0, after=30)
# Columns: Condition | Timing | Config | Quality | Location | Radiation | Key Clue
headers_m = ["Condition", "Timing", "Configuration", "Quality", "Location", "Radiation", "Key Clue / Cause"]
col_w_m = [Cm(3.2), Cm(1.7), Cm(2.4), Cm(2.0), Cm(2.4), Cm(2.4), Cm(3.3)]
murmurs = [
# (condition, timing_short, config, quality, location, radiation, clue, row_bg, label_color)
("Aortic Stenosis\n(AS)", "Systolic", "Crescendo-\ndecrescendo", "Harsh / rough",
"RUSB (2nd R ICS)", "Neck / carotids;\napex (Gallavardin)",
"Slow carotid pulse; S4; calcified valve", RED_L, RED),
("Mitral Regurgitation\n(MR)", "Holosystolic", "Plateau",
"Blowing, high-pitched", "Apex (5th ICS MCL)", "Left axilla / back",
"Soft S1; S3; displaced apex", PURPLE_L, PURPLE),
("Tricuspid Regurgitation\n(TR)", "Holosystolic", "Plateau",
"Blowing, soft", "LLSB (4thβ5th L ICS)", "Louder on inspiration\n(Carvallo's sign)",
"Pulsatile liver; raised JVP", AMBER_L, AMBER),
("Pulmonary Stenosis\n(PS)", "Systolic", "Crescendo-\ndecrescendo",
"Harsh", "LUSB (2nd L ICS)", "Left shoulder / back",
"Ejection click; wide split S2", BLUE_L, BLUE),
("VSD\n(small)", "Holosystolic", "Plateau",
"Harsh / loud", "Lower LSB", "Right parasternal",
"Maladie de Roger: louder = smaller VSD", BLUE_L, BLUE),
("HCM / HOCM", "Systolic", "Crescendo-\ndecrescendo",
"Harsh", "LLSB / between\napex & LLSB", "No radiation to neck",
"β Valsalva/standing; β squatting", PURPLE_L, PURPLE),
("Aortic Regurgitation\n(AR)", "Early diastolic", "Decrescendo",
"High-pitched,\nblowing", "LSB (Erb's point);\nleaning forward", "Apex (Austin Flint murmur)",
"Wide pulse pressure; Corrigan's pulse", RED_L, RED),
("Mitral Stenosis\n(MS)", "Mid-to-late\ndiastolic", "Rumble\n(low-pitched)",
"Low rumble\n(use bell)", "Apex; left lateral\ndecubitus position", "Does not radiate",
"Opening snap (OS); loud S1; AF common", BLUE_L, BLUE),
("Patent Ductus\nArteriosus (PDA)", "Continuous\n(S+D)", "Machinery\n(waxes/wanes)",
"Harsh,\nmachinery", "Left infraclavicular\n/ LUSB", "Left shoulder / clavicle",
"Peaks near S2; bounding pulse; prematurity", GREEN_L, GREEN),
("Innocent / Functional", "Systolic only", "Crescendo-\ndecrescendo",
"Soft, blowing,\nor vibratory", "Variable (often\nLLSB / apex)", "None",
"Asymptomatic; changes with posture; Grade IβII only", GREEN_L, GREEN),
]
t_mur = doc.add_table(rows=len(murmurs)+1, cols=7)
t_mur.alignment = WD_TABLE_ALIGNMENT.CENTER
set_table_borders(t_mur, color="BDC3C7", sz=4)
# Header
for i, h in enumerate(headers_m):
c = t_mur.cell(0, i)
set_cell_bg(c, NAVY)
p = c.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
set_para_spacing(p, before=50, after=50)
r = p.add_run(h)
r.font.bold = True; r.font.color.rgb = WHITE
r.font.size = Pt(8.5); r.font.name = "Calibri"
for i, (cond, timing, config, qual, loc, rad, clue, bg_c, lbl_c) in enumerate(murmurs):
row_idx = i + 1
cells = [cond, timing, config, qual, loc, rad, clue]
bgs = [bg_c, bg_c, bg_c, bg_c, bg_c, bg_c, bg_c]
bolds = [True, False, False, False, False, False, False]
colors= [lbl_c, GREY, GREY, GREY, GREY, GREY, GREY]
for j, (txt, bg_cv, bold, col) in enumerate(zip(cells, bgs, bolds, colors)):
c = t_mur.cell(row_idx, j)
set_cell_bg(c, bg_cv)
p = c.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.LEFT if j == 0 else WD_ALIGN_PARAGRAPH.LEFT
set_para_spacing(p, before=30, after=30)
r = p.add_run(txt)
r.font.bold = bold; r.font.color.rgb = col
r.font.size = Pt(8.5); r.font.name = "Calibri"
for row in t_mur.rows:
for i, cell in enumerate(row.cells):
cell.width = col_w_m[i]
doc.add_paragraph()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PAGE BREAK + SECTION 4 β LEVINE GRADING SCALE
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
page_break(doc)
add_heading(doc, "LEVINE GRADING SCALE (I β VI / VI)", level=1, color=TEAL)
grades = [
("I / VI", "Very faint; heard only with intense concentration", "No", "May be innocent", GREY_L, GREY),
("II / VI", "Soft but immediately audible", "No", "Often innocent", GREY_L, GREY),
("III / VI","Moderately loud", "No", "Usually structural", AMBER_L, AMBER),
("IV / VI", "Loud", "YES", "Structural / pathological", RED_L, RED),
("V / VI", "Very loud β heard with stethoscope edge only on chest","YES", "Pathological", RED_L, RED),
("VI / VI", "Heard with stethoscope OFF the chest", "YES", "Pathological", RED_L, RED),
]
t_gr = doc.add_table(rows=len(grades)+1, cols=4)
t_gr.alignment = WD_TABLE_ALIGNMENT.CENTER
set_table_borders(t_gr, color="BDC3C7", sz=4)
for i, h in enumerate(["Grade", "Examiner Experience", "Thrill?", "Clinical Note"]):
c = t_gr.cell(0, i)
set_cell_bg(c, NAVY)
p = c.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.CENTER
set_para_spacing(p, before=50, after=50)
r = p.add_run(h); r.font.bold = True; r.font.color.rgb = WHITE
r.font.size = Pt(9.5); r.font.name = "Calibri"
for i, (grade, desc, thrill, note, bg_c, lbl_c) in enumerate(grades):
for j, (txt, align, bold, col) in enumerate([
(grade, WD_ALIGN_PARAGRAPH.CENTER, True, lbl_c),
(desc, WD_ALIGN_PARAGRAPH.LEFT, False, GREY),
(thrill,WD_ALIGN_PARAGRAPH.CENTER, True, lbl_c),
(note, WD_ALIGN_PARAGRAPH.LEFT, False, GREY),
]):
c = t_gr.cell(i+1, j)
set_cell_bg(c, bg_c)
p = c.paragraphs[0]; p.alignment = align
set_para_spacing(p, before=40, after=40)
r = p.add_run(txt); r.font.bold = bold; r.font.color.rgb = col
r.font.size = Pt(9.5); r.font.name = "Calibri"
for row in t_gr.rows:
for i, cell in enumerate(row.cells):
cell.width = [Cm(1.8), Cm(8.0), Cm(2.0), Cm(5.6)][i]
add_body(doc, "Rule: Grade β₯ III or any diastolic / continuous murmur β Echocardiography always indicated.",
bold=True, color=RED, size=9.5, before=60, after=20)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 5 β BEDSIDE MANEUVERS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
add_heading(doc, "BEDSIDE MANEUVERS QUICK GUIDE", level=1, color=TEAL)
maneuvers = [
("Valsalva (strain phase)", "β preload β smaller LV",
"HCM β, MVP click earlier", "AS, MR, TR, VSD all β"),
("Squatting", "β preload and β afterload",
"AS, MR, TR β", "HCM β"),
("Standing from squat", "β venous return β β preload",
"HCM β, MVP click earlier", "AS, MR β"),
("Deep inspiration", "β right heart venous return",
"TR, TS, PS, PR β (Carvallo's sign)", "Left-sided murmurs relatively β"),
("Passive leg raise", "β venous return β β preload",
"Most left-sided murmurs β", "HCM β"),
("Hand grip (isometric)", "β afterload / SVR",
"MR, AR, VSD β", "AS, HCM β"),
("Amyl nitrite (inhaled)", "β afterload; β HR",
"AS, HCM, MS rumble β", "MR, AR, VSD β"),
]
t_man = doc.add_table(rows=len(maneuvers)+1, cols=4)
t_man.alignment = WD_TABLE_ALIGNMENT.CENTER
set_table_borders(t_man, color="BDC3C7", sz=4)
for i, h in enumerate(["Maneuver", "Physiology", "β Murmur", "β Murmur"]):
c = t_man.cell(0, i)
set_cell_bg(c, NAVY)
p = c.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.CENTER
set_para_spacing(p, before=50, after=50)
r = p.add_run(h); r.font.bold = True; r.font.color.rgb = WHITE
r.font.size = Pt(9.5); r.font.name = "Calibri"
for i, (man, phys, up, down) in enumerate(maneuvers):
bg_c = GREY_L if i % 2 == 0 else WHITE
vals = [man, phys, up, down]
cols_c = [NAVY, GREY, GREEN, RED]
bolds = [True, False, False, False]
for j, (txt, col, bold) in enumerate(zip(vals, cols_c, bolds)):
c = t_man.cell(i+1, j)
set_cell_bg(c, bg_c)
p = c.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.LEFT
set_para_spacing(p, before=40, after=40)
r = p.add_run(txt); r.font.bold = bold; r.font.color.rgb = col
r.font.size = Pt(9); r.font.name = "Calibri"
for row in t_man.rows:
for i, cell in enumerate(row.cells):
cell.width = [Cm(3.5), Cm(4.2), Cm(4.5), Cm(5.2)][i]
doc.add_paragraph()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PAGE BREAK + SECTION 6 β AUSCULTATION AREAS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
page_break(doc)
add_heading(doc, "AUSCULTATION AREAS (APETM Mnemonic)", level=1, color=TEAL)
areas_data = [
("A", "Aortic", "2nd right ICS, right sternal border", "Aortic Stenosis (AS)", RED_L, RED),
("P", "Pulmonary", "2nd left ICS, left sternal border", "Pulmonary Stenosis (PS), PDA", BLUE_L, BLUE),
("E", "Erb's Point", "3rd left ICS, left sternal border","Aortic Regurgitation (AR)\n(patient sits forward, breathes out)", RED_L, RED),
("T", "Tricuspid", "4thβ5th left ICS, left sternal border","TR, TS, VSD", AMBER_L, AMBER),
("M", "Mitral (Apex)", "5th left ICS, midclavicular line","MR, MS\n(use bell for MS; left lateral decubitus)", PURPLE_L, PURPLE),
]
t_ausc = doc.add_table(rows=len(areas_data)+1, cols=4)
t_ausc.alignment = WD_TABLE_ALIGNMENT.CENTER
set_table_borders(t_ausc, color="BDC3C7", sz=4)
for i, h in enumerate(["Letter", "Area", "Anatomical Location", "Murmur(s) Best Heard Here"]):
c = t_ausc.cell(0, i)
set_cell_bg(c, NAVY)
p = c.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.CENTER
set_para_spacing(p, before=50, after=50)
r = p.add_run(h); r.font.bold = True; r.font.color.rgb = WHITE
r.font.size = Pt(9.5); r.font.name = "Calibri"
for i, (letter, area, loc, murmur, bg_c, lbl_c) in enumerate(areas_data):
for j, (txt, col, bold, align) in enumerate([
(letter, lbl_c, True, WD_ALIGN_PARAGRAPH.CENTER),
(area, lbl_c, True, WD_ALIGN_PARAGRAPH.LEFT),
(loc, GREY, False, WD_ALIGN_PARAGRAPH.LEFT),
(murmur, GREY, False, WD_ALIGN_PARAGRAPH.LEFT),
]):
c = t_ausc.cell(i+1, j)
set_cell_bg(c, bg_c)
p = c.paragraphs[0]; p.alignment = align
set_para_spacing(p, before=40, after=40)
r = p.add_run(txt); r.font.bold = bold; r.font.color.rgb = col
r.font.size = Pt(10); r.font.name = "Calibri"
for row in t_ausc.rows:
for i, cell in enumerate(row.cells):
cell.width = [Cm(1.5), Cm(2.8), Cm(6.0), Cm(7.1)][i]
doc.add_paragraph()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 7 β INNOCENT vs PATHOLOGICAL
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
add_heading(doc, "INNOCENT vs PATHOLOGICAL MURMUR", level=1, color=TEAL)
t_ip = doc.add_table(rows=8, cols=2)
t_ip.alignment = WD_TABLE_ALIGNMENT.CENTER
set_table_borders(t_ip, color="BDC3C7", sz=4)
ip_data = [
("INNOCENT MURMUR", "PATHOLOGICAL MURMUR"),
("Grade I β II / VI only", "Grade III / VI or higher"),
("Systolic ONLY (never diastolic)", "Any diastolic or continuous murmur"),
("Short duration; no thrill", "Thrill may be present (Grade β₯ IV)"),
("No radiation to neck/axilla", "Radiation present"),
("Changes with posture / respiration", "Does not change / always present"),
("Asymptomatic child or young adult", "Symptoms: dyspnoea, syncope, chest pain"),
("No further workup if Grade IβII, asymptomatic, normal ECG/CXR",
"Echocardiography always indicated"),
]
for i, (left, right) in enumerate(ip_data):
bg_l = GREEN_L if i > 0 else GREEN
bg_r = RED_L if i > 0 else RED
fg_l = GREEN if i > 0 else WHITE
fg_r = RED if i > 0 else WHITE
for j, (txt, bg_c, fg_c, bold) in enumerate([
(left, bg_l, fg_l, i == 0),
(right, bg_r, fg_r, i == 0),
]):
c = t_ip.cell(i, j)
set_cell_bg(c, bg_c)
p = c.paragraphs[0]
set_para_spacing(p, before=40, after=40)
r = p.add_run(txt)
r.font.bold = bold; r.font.color.rgb = fg_c
r.font.size = Pt(9.5); r.font.name = "Calibri"
for row in t_ip.rows:
for cell in row.cells:
cell.width = Cm(8.7)
doc.add_paragraph()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SECTION 8 β MEMORY AIDS
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
add_heading(doc, "MEMORY AIDS & CLINICAL PEARLS", level=1, color=TEAL)
# Box-style memory aids using a 1-col table
def memory_box(doc, title, lines, bg_c, border_c):
t = doc.add_table(rows=1, cols=1)
t.alignment = WD_TABLE_ALIGNMENT.CENTER
c = t.cell(0, 0)
set_cell_bg(c, bg_c)
c.width = Cm(17.4)
p0 = c.paragraphs[0]
set_para_spacing(p0, before=60, after=20)
r0 = p0.add_run(" " + title)
r0.font.bold = True; r0.font.color.rgb = border_c
r0.font.size = Pt(10.5); r0.font.name = "Calibri"
for line in lines:
p = c.add_paragraph()
set_para_spacing(p, before=10, after=10)
r = p.add_run(" " + line)
r.font.size = Pt(9.5); r.font.name = "Calibri"; r.font.color.rgb = GREY
c.add_paragraph()
doc.add_paragraph()
memory_box(doc, "AUSCULTATION AREAS β 'All Patients Eat Tacos Mightily'",
["A β Aortic: 2nd Right ICS, right sternal border β Best for AS",
"P β Pulmonary: 2nd Left ICS, left sternal border β Best for PS, PDA",
"E β Erb's: 3rd Left ICS, left sternal border β Best for AR (lean forward, breathe out)",
"T β Tricuspid: 4thβ5th Left ICS, left sternal border β Best for TR, VSD",
"M β Mitral: 5th Left ICS, midclavicular (Apex) β Best for MR, MS"],
BLUE_L, BLUE)
memory_box(doc, "HCM vs AS β The Valsalva Trick",
["HCM: β with Valsalva / standing (less blood in LV β more obstruction)",
"AS: No significant change with Valsalva",
"Both: Crescendo-decrescendo systolic murmur β use Valsalva to distinguish"],
AMBER_L, AMBER)
memory_box(doc, "RIGHT-SIDED vs LEFT-SIDED MURMURS",
["Right-sided (TR, TS, PS, PR): ALL increase with INSPIRATION β Carvallo's Sign",
"Left-sided (MR, MS, AS, AR): Do NOT increase with inspiration",
"Trick: 'Inspiration β Right side | Expiration β Left side'"],
GREEN_L, GREEN)
memory_box(doc, "THE DIASTOLIC MURMUR RULE",
["Any diastolic murmur is ALWAYS pathological",
"Always order echocardiography for any diastolic murmur without exception",
"Early diastolic (decrescendo) β AR or PR",
"Mid/late diastolic (low rumble) + opening snap β MS (rheumatic fever)"],
RED_L, RED)
memory_box(doc, "THE MURMUR CONFIGURATION RULES",
["Crescendo-decrescendo (diamond / ejection): AS, PS, HCM, Innocent",
"Plateau (holosystolic / uniform): MR, TR, VSD",
"Decrescendo (fades): AR, PR (early diastolic)",
"Low rumble (mid/late diastolic): MS, TS",
"Machinery (continuous): PDA"],
PURPLE_L, PURPLE)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PAGE BREAK + SECTION 9 β INDIVIDUAL DIAGNOSIS SUMMARIES
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
page_break(doc)
add_heading(doc, "INDIVIDUAL DIAGNOSIS SUMMARIES", level=1, color=TEAL)
add_body(doc, "Each box below summarises one diagnosis from the flowchart. Use these for rapid revision.",
italic=True, size=9, before=0, after=20)
dx_entries = [
("Aortic Stenosis (AS)", RED, RED_L, [
"Timing: Systolic ejection (crescendo-decrescendo)",
"Location: RUSB (2nd right ICS) | Radiation: Neck/carotids; apex (Gallavardin phenomenon)",
"Quality: Harsh, rough | Grade: IIβV/VI",
"Key clues: Slow-rising carotid pulse (pulsus parvus et tardus); S4; delayed/soft A2",
"Causes: Calcific degeneration (elderly); bicuspid AoV (young adults); rheumatic fever",
"Symptoms triad: Angina + Syncope + Dyspnoea = surgical urgency",
"Management: SAVR or TAVR when symptomatic or severe LV dysfunction",
]),
("Mitral Regurgitation (MR)", PURPLE, PURPLE_L, [
"Timing: Holosystolic (pansystolic) β plateau from S1 to S2",
"Location: Apex | Radiation: Left axilla / back",
"Quality: Blowing, high-pitched | Grade: IIβIV/VI",
"Key clues: Soft S1; S3 (volume overload); displaced PMI",
"Causes: MVP (most common in developed world), rheumatic fever, IE, papillary muscle rupture (post-MI)",
"Management: Afterload reduction (ACEi). Severe/symptomatic: Mitral repair (preferred) or replacement",
]),
("Tricuspid Regurgitation (TR)", AMBER, AMBER_L, [
"Timing: Holosystolic β plateau",
"Location: LLSB (4thβ5th L ICS) | Radiation: Louder on INSPIRATION (Carvallo's sign)",
"Quality: Blowing, soft | Grade: IβIII/VI",
"Key clues: Pulsatile liver; raised JVP with large 'v' wave; peripheral oedema",
"Causes: Secondary (RV dilation β most common); IE in IV drug users; carcinoid; rheumatic fever",
"Management: Treat underlying cause (diuretics for RV failure). Severe: Annuloplasty / replacement",
]),
("Pulmonary Stenosis (PS)", BLUE, BLUE_L, [
"Timing: Systolic ejection (crescendo-decrescendo)",
"Location: LUSB (2nd left ICS) | Radiation: Left shoulder / back",
"Quality: Harsh, blowing | Grade: IIβIV/VI",
"Key clues: Ejection click that disappears on inspiration; widely split S2; RV heave",
"Causes: Congenital (most common); carcinoid syndrome",
"Management: Mild: observe. Severe: Balloon pulmonary valvuloplasty",
]),
("Ventricular Septal Defect (VSD)", BLUE, BLUE_L, [
"Timing: Holosystolic β plateau",
"Location: Lower left sternal border | Radiation: Right of sternum",
"Quality: Harsh, loud | Grade: IIIβV/VI",
"Key clues: Maladie de Roger β louder murmur = smaller defect; thrill common",
"Causes: Congenital; post-MI ventricular septal rupture (surgical emergency)",
"Management: Small VSD may close spontaneously. Large: catheter-based or surgical closure",
]),
("Hypertrophic Cardiomyopathy (HCM / HOCM)", PURPLE, PURPLE_L, [
"Timing: Systolic ejection (crescendo-decrescendo)",
"Location: LLSB / between apex and LLSB | Radiation: None to neck (unlike AS)",
"Quality: Harsh | Grade: IIβIV/VI",
"Key clues: β with Valsalva and standing; β with squatting and hand-grip; bisferiens pulse",
"Causes: Autosomal dominant sarcomere mutations (MYH7, MYBPC3); most common SCD cause in young athletes",
"Management: Beta-blockers or verapamil. ICD if SCD risk features. Septal myectomy / alcohol ablation",
]),
("Aortic Regurgitation (AR)", RED, RED_L, [
"Timing: Early diastolic β decrescendo (starts immediately after S2)",
"Location: Left sternal border / Erb's point | Best heard: leaning forward, breathed out",
"Radiation: Apex (Austin Flint murmur β functional MS from regurgitant jet)",
"Quality: High-pitched, blowing, soft | Grade: IβIII/VI",
"Key clues: Wide pulse pressure; Corrigan's (water hammer) pulse; de Musset's sign (head nod); Quincke's",
"Causes: Aortic root dilation (Marfan, HTN); IE; bicuspid AoV; rheumatic fever",
"Management: Chronic: ACEi. Severe + symptomatic or EF < 50%: Aortic valve replacement",
]),
("Mitral Stenosis (MS)", BLUE, BLUE_L, [
"Timing: Mid-to-late diastolic β low rumble; presystolic accentuation in sinus rhythm",
"Location: Apex | Patient: Left lateral decubitus | Use: Bell of stethoscope",
"Radiation: Does not radiate",
"Quality: Low-pitched rumble | Grade: IβIII/VI",
"Key clues: Opening snap (OS) just after S2 β shorter OS-S2 gap = more severe; loud S1; AF common",
"Causes: Rheumatic fever (Group A Strep β antibody cross-reaction with mitral valve)",
"Management: PMBV (percutaneous balloon valvotomy) if suitable anatomy. Anticoagulate if AF",
]),
("Patent Ductus Arteriosus (PDA)", GREEN, GREEN_L, [
"Timing: Continuous β through both systole AND diastole",
"Location: Left infraclavicular / LUSB | Radiation: Left shoulder / clavicle",
"Quality: Harsh, machinery-like β waxes in systole, wanes in diastole, peaks near S2",
"Grade: IIβIV/VI",
"Key clues: Bounding pulse; wide pulse pressure; machinery murmur is pathognomonic",
"Causes: Prematurity; congenital rubella; prostaglandin-dependent circulation",
"Management: Premature infants: Indomethacin / Ibuprofen. Term/children: Catheter occlusion device or ligation",
]),
("Innocent / Functional Murmur", GREEN, GREEN_L, [
"Timing: Systolic ONLY β NEVER diastolic",
"Location: Variable (often LLSB or apex) | Radiation: None",
"Quality: Soft, blowing, or vibratory (Still's murmur in children) | Grade: IβII/VI only",
"Key clues: Asymptomatic; changes with posture; no thrill; no other cardiac signs; normal ECG/CXR",
"Causes: High cardiac output states β fever, anemia, pregnancy, exercise, hyperthyroidism",
"Management: Reassurance. No echocardiography needed if Grade IβII, systolic, asymptomatic, normal CXR/ECG",
]),
]
for (dx_title, bdr_c, bg_c, bullet_lines) in dx_entries:
# Title row
t_dx = doc.add_table(rows=1, cols=1)
t_dx.alignment = WD_TABLE_ALIGNMENT.CENTER
hc = t_dx.cell(0, 0)
set_cell_bg(hc, bdr_c)
hc.width = Cm(17.4)
p = hc.paragraphs[0]
set_para_spacing(p, before=50, after=50)
r = p.add_run(" " + dx_title)
r.font.bold = True; r.font.color.rgb = WHITE
r.font.size = Pt(11); r.font.name = "Calibri"
# Content rows
t_body = doc.add_table(rows=1, cols=1)
t_body.alignment = WD_TABLE_ALIGNMENT.CENTER
bc = t_body.cell(0, 0)
set_cell_bg(bc, bg_c)
bc.width = Cm(17.4)
first = True
for line in bullet_lines:
p2 = bc.paragraphs[0] if first else bc.add_paragraph()
first = False
set_para_spacing(p2, before=15, after=15)
r2 = p2.add_run(" β’ " + line)
r2.font.size = Pt(9.5); r2.font.name = "Calibri"; r2.font.color.rgb = GREY
bc.add_paragraph()
doc.add_paragraph()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# FOOTER
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
t_foot = doc.add_table(rows=1, cols=1)
c = t_foot.cell(0, 0)
set_cell_bg(c, NAVY)
p = c.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
set_para_spacing(p, before=50, after=50)
r = p.add_run(
"Sources: Harrison's Principles of Internal Medicine 22E (2025) Β· "
"Guyton and Hall Textbook of Medical Physiology Β· "
"Fuster and Hurst's The Heart 15E Β· Goldman-Cecil Medicine"
" | Orris Medical AI Β· July 2026 Β· For educational use only"
)
r.font.size = Pt(7.5); r.font.color.rgb = RGBColor(0xB0, 0xD0, 0xE8)
r.font.name = "Calibri"
# ββ Save ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
doc.save(OUTPUT)
import os
size = os.path.getsize(OUTPUT)
print(f"Saved: {OUTPUT}")
print(f"Size: {size/1024:.1f} KB")
cd /tmp/workspace/murmur-reference && python build_handout.py 2>&1
Heart Murmur Handout
Word Document Β· DOCX