How to approach for exam of ent
| Domain | Key Points |
|---|---|
| Chief complaint | Duration, onset, progression |
| Tobacco | Pack-years, type (cigarettes/cigars/smokeless), quit date |
| Alcohol | Amount, frequency, beverage type, duration |
| Drug/IV use | Current and past |
| Sexual history | HPV risk (relevant to head and neck cancers), HIV, hepatitis C |
| Occupational exposure | Noise, carcinogens, irritants, radiation history |
| Family history | Hereditary hearing loss, congenital defects, atopy, cancer |
| Review of systems | Respiratory, cardiac, neurologic, endocrine, GI, urogenital, musculoskeletal, skin, psychiatric |

| Test | How to Perform | Normal | Conductive HL | Sensorineural HL |
|---|---|---|---|---|
| Weber | Place vibrating fork on forehead/midline | Midline ("negative") | Lateralizes to affected ear | Lateralizes to better ear |
| Rinne | Air conduction (fork near canal) vs. bone conduction (mastoid) | AC > BC (Rinne positive) | BC > AC (Rinne negative) | AC > BC but both reduced |
| Level | Region |
|---|---|
| I | Submental + submandibular |
| II | Upper jugular (angle of mandible to hyoid) |
| III | Mid-jugular (hyoid to cricoid) |
| IV | Lower jugular (cricoid to clavicle) |
| V | Posterior triangle |
| VI | Central compartment (paratracheal, pretracheal) |
| CN | Test |
|---|---|
| I (Olfactory) | Identify common smells unilaterally |
| V (Trigeminal) | Facial sensation in V1/V2/V3 distributions; corneal reflex |
| VII (Facial) | Forehead, eye closure, smile, brow raise - note central vs. peripheral pattern |
| VIII (Vestibulocochlear) | Tuning forks (hearing), Romberg, Unterberger test (vestibular) |
| IX/X (Glosso/Vagus) | Gag reflex, palate elevation, phonation ("aah") |
| XI (Accessory) | Sternocleidomastoid and trapezius strength |
| XII (Hypoglossal) | Tongue protrusion - deviates toward weak side |
Make a study guide PDF on ENT examination tips and mnemonics
mkdir -p /home/daytona/workspace/ent-study-guide && ls /home/daytona/workspace/e…
~/ent-study-guide/generate_ent_guide.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm, cm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import Flowable
# ── Colour palette ──────────────────────────────────────────────────────────
DARK_TEAL = colors.HexColor("#006D7A")
MID_TEAL = colors.HexColor("#009CAB")
LIGHT_TEAL = colors.HexColor("#E0F5F7")
ORANGE = colors.HexColor("#E87722")
LIGHT_ORANGE= colors.HexColor("#FFF3E8")
PURPLE = colors.HexColor("#6A3D9A")
LIGHT_PURPLE= colors.HexColor("#F3EEFF")
GREEN = colors.HexColor("#2E7D32")
LIGHT_GREEN = colors.HexColor("#E8F5E9")
RED = colors.HexColor("#C62828")
LIGHT_RED = colors.HexColor("#FFEBEE")
YELLOW_BG = colors.HexColor("#FFFDE7")
GREY_BG = colors.HexColor("#F5F5F5")
WHITE = colors.white
BLACK = colors.black
PAGE_W, PAGE_H = A4
MARGIN = 18 * mm
# ── Style helpers ────────────────────────────────────────────────────────────
base_styles = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
TITLE_STYLE = S("Title",
fontName="Helvetica-Bold", fontSize=28,
textColor=WHITE, alignment=TA_CENTER, leading=34, spaceAfter=4)
SUBTITLE_STYLE = S("Subtitle",
fontName="Helvetica", fontSize=13,
textColor=colors.HexColor("#CCF0F3"), alignment=TA_CENTER, leading=18)
H1 = S("H1",
fontName="Helvetica-Bold", fontSize=14,
textColor=WHITE, alignment=TA_LEFT, leading=18, spaceAfter=0)
H2 = S("H2",
fontName="Helvetica-Bold", fontSize=11,
textColor=DARK_TEAL, alignment=TA_LEFT, leading=15, spaceAfter=2, spaceBefore=4)
H3 = S("H3",
fontName="Helvetica-Bold", fontSize=10,
textColor=ORANGE, alignment=TA_LEFT, leading=13, spaceAfter=2, spaceBefore=3)
BODY = S("Body",
fontName="Helvetica", fontSize=9,
textColor=BLACK, alignment=TA_LEFT, leading=13, spaceAfter=2)
BODY_J = S("BodyJ",
fontName="Helvetica", fontSize=9,
textColor=BLACK, alignment=TA_JUSTIFY, leading=13, spaceAfter=2)
SMALL = S("Small",
fontName="Helvetica", fontSize=8,
textColor=colors.HexColor("#444444"), alignment=TA_LEFT, leading=11)
BOLD_BODY = S("BoldBody",
fontName="Helvetica-Bold", fontSize=9,
textColor=BLACK, alignment=TA_LEFT, leading=13)
MNEMONIC_TITLE = S("MnemonicTitle",
fontName="Helvetica-Bold", fontSize=10,
textColor=PURPLE, alignment=TA_LEFT, leading=14, spaceAfter=1)
MNEMONIC_BODY = S("MnemonicBody",
fontName="Helvetica", fontSize=9,
textColor=BLACK, alignment=TA_LEFT, leading=13, spaceAfter=1)
WARN = S("Warn",
fontName="Helvetica-Bold", fontSize=9,
textColor=RED, alignment=TA_LEFT, leading=13)
CENTER_SM = S("CenterSm",
fontName="Helvetica", fontSize=8,
textColor=colors.HexColor("#555555"), alignment=TA_CENTER, leading=11)
# ── Reusable building blocks ─────────────────────────────────────────────────
def section_header(title, color=DARK_TEAL):
tbl = Table([[Paragraph(title, H1)]], colWidths=[PAGE_W - 2*MARGIN])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING",(0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING",(0,0), (-1,-1), 10),
("ROUNDEDCORNERS", [4]),
]))
return tbl
def tip_box(text, bg=LIGHT_TEAL, border=MID_TEAL):
tbl = Table([[Paragraph(f"<b>Tip:</b> {text}", BODY)]], colWidths=[PAGE_W - 2*MARGIN])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("LINEABOVE", (0,0), (-1, 0), 2, border),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0),(-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING",(0,0), (-1,-1), 8),
]))
return tbl
def mnemonic_box(acronym, expansion_rows, bg=LIGHT_PURPLE, title_color=PURPLE):
"""
expansion_rows: list of (letter, word, meaning) tuples
"""
header = [Paragraph(f"<b>{acronym}</b>", S("Mn",
fontName="Helvetica-Bold", fontSize=11, textColor=title_color, leading=15))]
data = [header]
for letter, word, meaning in expansion_rows:
row = [Paragraph(
f"<b><font color='#{title_color.hexval()[2:]}' size='10'>{letter}</font></b> "
f"<b>{word}</b> - <i>{meaning}</i>", BODY)]
data.append(row)
tbl = Table(data, colWidths=[PAGE_W - 2*MARGIN])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("LINEABOVE", (0,0), (0, 0), 2, title_color),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING",(0,0),(-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING",(0,0), (-1,-1), 8),
]))
return tbl
def two_col_table(left_rows, right_rows, col_header_left="", col_header_right="",
bg_header=DARK_TEAL, bg_row=GREY_BG):
max_rows = max(len(left_rows), len(right_rows))
while len(left_rows) < max_rows: left_rows.append(("",""))
while len(right_rows) < max_rows: right_rows.append(("",""))
col_w = (PAGE_W - 2*MARGIN - 6*mm) / 2
data = []
if col_header_left or col_header_right:
data.append([
Paragraph(col_header_left, S("TH", fontName="Helvetica-Bold", fontSize=9,
textColor=WHITE, leading=12)),
Paragraph(col_header_right, S("TH2", fontName="Helvetica-Bold", fontSize=9,
textColor=WHITE, leading=12)),
])
for (k1, v1), (k2, v2) in zip(left_rows, right_rows):
data.append([
Paragraph(f"<b>{k1}</b> {v1}", BODY) if k1 else Paragraph("", BODY),
Paragraph(f"<b>{k2}</b> {v2}", BODY) if k2 else Paragraph("", BODY),
])
tbl = Table(data, colWidths=[col_w, col_w])
style = [
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING",(0,0),(-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING",(0,0), (-1,-1), 6),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
("ROWBACKGROUNDS", (0, 1 if (col_header_left or col_header_right) else 0),
(-1,-1), [WHITE, bg_row]),
]
if col_header_left or col_header_right:
style += [
("BACKGROUND", (0,0), (-1,0), bg_header),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
]
tbl.setStyle(TableStyle(style))
return tbl
def simple_table(headers, rows, col_widths=None, header_bg=DARK_TEAL):
col_w = col_widths or [(PAGE_W - 2*MARGIN) / len(headers)] * len(headers)
data = [[Paragraph(h, S("TH3", fontName="Helvetica-Bold", fontSize=9,
textColor=WHITE, leading=12)) for h in headers]]
for row in rows:
data.append([Paragraph(str(c), BODY) for c in row])
tbl = Table(data, colWidths=col_w)
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), header_bg),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, GREY_BG]),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING",(0,0),(-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING",(0,0), (-1,-1), 6),
]))
return tbl
def bullet(text, indent=0):
return Paragraph(f"{' '*indent*4}<b>•</b> {text}", BODY)
def subbullet(text):
return bullet(text, indent=2)
def warn_box(text):
tbl = Table([[Paragraph(f"⚠ {text}", WARN)]], colWidths=[PAGE_W - 2*MARGIN])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_RED),
("LINEABOVE", (0,0), (-1, 0), 2, RED),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0),(-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING",(0,0), (-1,-1), 8),
]))
return tbl
def sp(n=4): return Spacer(1, n*mm)
# ── COVER PAGE ───────────────────────────────────────────────────────────────
def build_cover():
elements = []
# Title banner
banner_data = [[
Paragraph("ENT EXAMINATION", TITLE_STYLE),
],[
Paragraph("Study Guide: Tips & Mnemonics", SUBTITLE_STYLE),
],[
Paragraph("Otolaryngology · Head & Neck Surgery", S("Sub2",
fontName="Helvetica", fontSize=11,
textColor=colors.HexColor("#A8E6EC"), alignment=TA_CENTER, leading=16)),
]]
banner = Table(banner_data, colWidths=[PAGE_W - 2*MARGIN])
banner.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), DARK_TEAL),
("TOPPADDING", (0,0), (-1,-1), 14),
("BOTTOMPADDING",(0,0),(-1,-1), 14),
("LEFTPADDING", (0,0), (-1,-1), 20),
("RIGHTPADDING",(0,0), (-1,-1), 20),
]))
elements.append(banner)
elements.append(sp(6))
# Quick-reference summary boxes
summary = [
("EAR", "Otoscopy · Tuning Forks\nAudiometry · Vestibular Tests", MID_TEAL),
("NOSE", "Rhinoscopy · Endoscopy\nSinus Assessment", ORANGE),
("THROAT", "Oral Cavity · Larynx\nNeck Palpation", PURPLE),
]
cols = []
for title, body, col in summary:
cell = Table([
[Paragraph(title, S("ST", fontName="Helvetica-Bold", fontSize=13,
textColor=WHITE, alignment=TA_CENTER, leading=16))],
[Paragraph(body, S("SB", fontName="Helvetica", fontSize=9,
textColor=WHITE, alignment=TA_CENTER, leading=13))],
], colWidths=[(PAGE_W - 2*MARGIN - 8*mm)/3])
cell.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), col),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING",(0,0),(-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING",(0,0), (-1,-1), 6),
]))
cols.append(cell)
summary_tbl = Table([cols], colWidths=[(PAGE_W - 2*MARGIN - 8*mm)/3]*3,
hAlign="CENTER")
summary_tbl.setStyle(TableStyle([
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING",(0,0), (-1,-1), 0),
("INNERGRID", (0,0), (-1,-1), 3, WHITE),
]))
elements.append(summary_tbl)
elements.append(sp(5))
# What's inside
what_data = [
[Paragraph("WHAT'S INSIDE", S("WI", fontName="Helvetica-Bold", fontSize=10,
textColor=DARK_TEAL, alignment=TA_CENTER, leading=14))],
[Paragraph(
"History-Taking Framework • General Appearance • Ear Examination • "
"Tuning Fork Tests • Nose & Sinuses • Throat & Oral Cavity • "
"Larynx • Neck Levels • Cranial Nerve Quick-Check • Vestibular Tests • "
"Mnemonics for Everything • High-Yield Exam Tips",
S("WIB", fontName="Helvetica", fontSize=9,
textColor=colors.HexColor("#333333"), alignment=TA_CENTER, leading=14))],
]
what_tbl = Table(what_data, colWidths=[PAGE_W - 2*MARGIN])
what_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), YELLOW_BG),
("LINEABOVE", (0,0), (-1,0), 2, ORANGE),
("LINEBELOW", (0,-1), (-1,-1), 2, ORANGE),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING",(0,0),(-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 12),
("RIGHTPADDING",(0,0), (-1,-1), 12),
]))
elements.append(what_tbl)
elements.append(sp(4))
elements.append(Paragraph("Based on Cummings Otolaryngology: Head and Neck Surgery",
CENTER_SM))
elements.append(sp(1))
elements.append(Paragraph("For MBBS / MD / MRCS / Postgraduate ENT Examinations",
CENTER_SM))
elements.append(PageBreak())
return elements
# ── SECTION 1 - HISTORY ──────────────────────────────────────────────────────
def build_history():
e = []
e.append(section_header("1. HISTORY TAKING IN ENT"))
e.append(sp(3))
e.append(Paragraph("Framework: The SOCRATES+ Approach", H2))
e.append(simple_table(
["Domain", "Key Questions"],
[
["Chief Complaint", "Site · Onset · Character · Radiation · Associated symptoms · Timing · Exacerbating/relieving factors · Severity"],
["Tobacco", "Pack-years, type (cigarette/cigar/smokeless), quit date"],
["Alcohol", "Amount, frequency, beverage type, duration"],
["Occupational Exposure", "Noise (work/recreational), carcinogens, irritants, radiation history"],
["Sexual History", "HPV risk for head & neck cancer, HIV, Hepatitis C status"],
["Family History", "Hereditary hearing loss, congenital defects, atopy, cancer"],
["Review of Systems", "Respiratory, cardiac, neurologic, endocrine, GI, urogenital, skin, psychiatric"],
],
col_widths=[55*mm, PAGE_W - 2*MARGIN - 55*mm]
))
e.append(sp(3))
e.append(mnemonic_box("SOCRATES (for any ENT symptom)",
[
("S", "Site", "Ear, nose, throat, neck?"),
("O", "Onset", "Sudden / gradual?"),
("C", "Character", "Quality of symptom (sharp, dull, muffled, blocked)"),
("R", "Radiation", "Otalgia radiating from throat? Neck to ear?"),
("A", "Associated", "Tinnitus, vertigo, discharge, dysphagia, hoarseness"),
("T", "Timing", "Constant/intermittent, diurnal variation"),
("E", "Exacerbating", "Cold, allergies, posture, movement"),
("S", "Severity", "Effect on daily activities, sleep, work"),
]
))
e.append(sp(3))
e.append(tip_box("In head and neck cancer history always ask: tobacco + alcohol + HPV exposure "
"(sexual history). These are the 'Big 3' risk factors."))
e.append(PageBreak())
return e
# ── SECTION 2 - EAR EXAM ─────────────────────────────────────────────────────
def build_ear():
e = []
e.append(section_header("2. EAR EXAMINATION", color=MID_TEAL))
e.append(sp(3))
e.append(Paragraph("Step-by-Step Sequence", H2))
steps = [
("1", "Inspect auricle", "Deformity, skin lesions, pre-auricular pits/sinuses, keloids, Battle's sign"),
("2", "Palpate", "Tragal tenderness (otitis externa) · Mastoid tenderness (mastoiditis)"),
("3", "Straighten canal", "Adults: pull pinna UP + BACK · Children: pull DOWN + BACK"),
("4", "Otoscopy", "Canal then tympanic membrane (TM) — see landmarks below"),
("5", "Pneumatic otoscopy", "Seal canal, apply gentle pressure; normal TM moves freely"),
("6", "Tuning fork tests", "Weber (512 Hz), Rinne (512 Hz)"),
("7", "Hearing screen", "Whisper test at 60 cm if audiometer unavailable"),
]
step_data = [[
Paragraph(f"<b>{s}</b>", S("StN", fontName="Helvetica-Bold", fontSize=11,
textColor=WHITE, alignment=TA_CENTER, leading=14)),
Paragraph(f"<b>{t}</b>", BOLD_BODY),
Paragraph(d, BODY),
] for s, t, d in steps]
step_tbl = Table(step_data, colWidths=[10*mm, 45*mm, PAGE_W-2*MARGIN-55*mm])
step_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,-1), MID_TEAL),
("ROWBACKGROUNDS", (1,0), (-1,-1), [WHITE, LIGHT_TEAL]),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0),(-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING",(0,0), (-1,-1), 6),
("ALIGN", (0,0), (0,-1), "CENTER"),
]))
e.append(step_tbl)
e.append(sp(4))
e.append(Paragraph("Tympanic Membrane Landmarks (MUST KNOW)", H2))
e.append(mnemonic_box("PUMP-AC (TM landmarks, superior to inferior)",
[
("P", "Pars flaccida", "Superior, thin portion — site of acquired cholesteatoma"),
("U", "Umbo", "Tip of malleus handle — the central dimple"),
("M", "Malleus handle", "Runs from lateral process to umbo"),
("P", "Pars tensa", "Main bulk of TM; subdivided into 4 quadrants"),
("A", "Annulus", "Fibrocartilaginous ring attaching TM to bony canal"),
("C", "Cone of light", "Anterior-inferior bright reflection (5 o'clock right, 7 o'clock left)"),
],
bg=LIGHT_TEAL, title_color=MID_TEAL
))
e.append(sp(3))
e.append(Paragraph("TM Findings and Their Significance", H2))
e.append(simple_table(
["Finding", "Appearance", "Diagnosis"],
[
["Myringosclerosis", "Chalky white patches", "Previous trauma/infection"],
["Myringitis", "Thickened, red TM ± bullae", "Acute otitis media / viral"],
["Atelectasis", "Thin TM draped over ossicles", "Adhesive otitis media"],
["Acquired cholesteatoma", "White mass + granulation tissue + perforation", "Posterior-superior marginal perforation"],
["Congenital cholesteatoma", "White pearl behind intact TM, ant-sup quadrant", "Keratin cyst (congenital)"],
["Serous effusion", "Amber fluid, air-fluid levels", "Otitis media with effusion"],
["Mucoid effusion", "Dull grey-white, retracted TM", "Glue ear (OME)"],
["Glomus tumor", "Vascular mass; Brown sign on pneumatic otoscopy", "Glomus tympanicum"],
["Perforations", "Central / marginal / attic - note size as % of TM", "Chronic suppurative otitis media"],
],
col_widths=[42*mm, 55*mm, PAGE_W-2*MARGIN-97*mm]
))
e.append(sp(4))
e.append(Paragraph("Tuning Fork Tests", H2))
e.append(simple_table(
["Test", "Method", "Normal", "Conductive HL", "Sensorineural HL"],
[
["Weber\n(512 Hz)", "Place on forehead / midline teeth", "Midline (negative)", "Lateralises to AFFECTED ear", "Lateralises to BETTER ear"],
["Rinne\n(512 Hz)", "AC (fork near canal) vs BC (mastoid)", "AC > BC (Positive)", "BC > AC (Negative)", "AC > BC but both reduced"],
["Absolute Bone\nConduction", "Compare patient BC to examiner BC", "Equal to examiner", "Equal to examiner (normal cochlea)", "Reduced (cochlear loss)"],
],
col_widths=[22*mm, 48*mm, 30*mm, 40*mm, PAGE_W-2*MARGIN-140*mm]
))
e.append(sp(3))
e.append(mnemonic_box("WEBER RULE: 'Worse Ear Wins for Conductive'",
[
("W", "Weber to worse ear", "= Conductive hearing loss on that side"),
("B", "Better ear gets Weber", "= Sensorineural hearing loss on opposite side"),
("R", "Rinne Negative", "= BC > AC = Conductive loss"),
("P", "Positive Rinne", "= AC > BC = Normal or sensorineural"),
]
))
e.append(sp(3))
e.append(warn_box("False Negative Rinne: Occurs in severe unilateral sensorineural loss "
"- sound from mastoid crosses skull to better cochlea. "
"Always correlate Rinne with Weber!"))
e.append(PageBreak())
return e
# ── SECTION 3 - NOSE ─────────────────────────────────────────────────────────
def build_nose():
e = []
e.append(section_header("3. NOSE EXAMINATION", color=ORANGE))
e.append(sp(3))
e.append(Paragraph("Sequence", H2))
sequence = [
("External Inspection", "Deviation, deformity, skin lesions, nasal tip support, columellar show, nasal valve collapse on inspiration"),
("Anterior Rhinoscopy", "Speculum opened VERTICALLY; inspect: septum, inferior turbinate, middle turbinate, mucosa colour, discharge, polyps"),
("Nasal Endoscopy", "Systematic 3-pass protocol (see below)"),
("Olfactory Test", "Ask patient to identify common smells unilaterally (CN I)"),
("Nasal Patency", "Ask patient to breathe through each nostril; or use cold spatula/mirror for misting"),
]
for title, body in sequence:
e.append(Paragraph(title, H3))
e.append(Paragraph(body, BODY))
e.append(sp(2))
e.append(tip_box("Open the Thudichum speculum VERTICALLY (blades go up/down), NOT horizontally. "
"Horizontal opening crushes the septum and causes pain."))
e.append(sp(3))
e.append(Paragraph("Nasal Endoscopy: The 3-Pass Protocol", H2))
e.append(simple_table(
["Pass", "Structures Visualised", "What to Look For"],
[
["1st Pass", "Floor, inferior meatus, inferior turbinate, nasopharynx, Eustachian tube orifice, adenoids", "Inferior turbinate hypertrophy, post-nasal drip, adenoid enlargement, nasopharyngeal mass"],
["2nd Pass", "Middle meatus, middle turbinate, uncinate process, ostiomeatal complex", "Polyps, purulent discharge, fungal concretions, anatomic variants (concha bullosa)"],
["3rd Pass", "Superior turbinate, sphenoethmoidal recess, sphenoid ostium", "CSF leak, posterior polyps, sphenoid sinusitis"],
],
col_widths=[14*mm, 75*mm, PAGE_W-2*MARGIN-89*mm]
))
e.append(sp(3))
e.append(Paragraph("Nasal Mucosa Colour Guide", H2))
col_data = [
[Paragraph("<b>Colour</b>", BOLD_BODY), Paragraph("<b>Significance</b>", BOLD_BODY)],
[Paragraph("Pale / bluish-grey", BODY), Paragraph("Allergic rhinitis", BODY)],
[Paragraph("Erythematous / red", BODY), Paragraph("Acute infection, vasomotor rhinitis", BODY)],
[Paragraph("Pale + oedematous polyps", BODY), Paragraph("Chronic rhinosinusitis with polyps (CRSwNP)", BODY)],
[Paragraph("Dry + crusting", BODY), Paragraph("Atrophic rhinitis (ozaena), Sjogren's, cocaine use", BODY)],
[Paragraph("Perforation of septum", BODY), Paragraph("Cocaine, GPA (Wegener's), trauma, iatrogenic", BODY)],
]
col_tbl = Table(col_data, colWidths=[(PAGE_W-2*MARGIN)/2]*2)
col_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), ORANGE),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_ORANGE]),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING",(0,0),(-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING",(0,0), (-1,-1), 6),
]))
e.append(col_tbl)
e.append(sp(3))
e.append(mnemonic_box("SEPTUM (causes of septal perforation)",
[
("S", "Surgery / Septal haematoma", "Post-operative, untreated haematoma"),
("E", "External trauma", "Nasal fractures"),
("P", "Perforation by cocaine", "Nasal cocaine use — classic cause"),
("T", "Tuberculosis / Syphilis", "Granulomatous infection"),
("U", "Ulceration (GPA/Wegener's)", "Granulomatosis with polyangiitis"),
("M", "Malignancy", "Lymphoma, squamous cell carcinoma"),
],
bg=LIGHT_ORANGE, title_color=ORANGE
))
e.append(PageBreak())
return e
# ── SECTION 4 - THROAT ───────────────────────────────────────────────────────
def build_throat():
e = []
e.append(section_header("4. THROAT / ORAL CAVITY EXAMINATION", color=PURPLE))
e.append(sp(3))
e.append(Paragraph("Systematic Oral Cavity Inspection", H2))
oral = [
("Lips", "Ulcers, crusting, angular cheilitis, carcinoma"),
("Buccal mucosa", "Leukoplakia, erythroplakia, lichen planus, Stensen's duct orifice"),
("Upper and lower alveolus", "Gingival swellings, extraction sockets, loose teeth"),
("Hard palate", "Torus palatinus, perforations, mucosal lesions"),
("Floor of mouth", "Ranula, Wharton's duct, submandibular calculi — PALPATE bimanually"),
("Tongue", "Dorsum (colour, texture), lateral borders (carcinoma), ventral surface, mobility, fasciculations"),
("Soft palate", "Uvula deviation, palatal petechiae, submucous cleft"),
("Tonsils", "Size (grade 1-4), crypts, exudate, ulceration, asymmetry"),
("Posterior pharyngeal wall", "Mucosal irregularity, post-nasal drip, lymphoid hyperplasia"),
]
e.append(simple_table(
["Structure", "Key Findings"],
oral,
col_widths=[45*mm, PAGE_W-2*MARGIN-45*mm]
))
e.append(sp(3))
e.append(mnemonic_box("LOTS (causes of unilateral tonsillar enlargement)",
[
("L", "Lymphoma", "Most feared - irregular, hard, non-tender"),
("O", "Other malignancy", "Squamous cell carcinoma (often ulcerated)"),
("T", "Tonsillar abscess (peritonsillar)", "Trismus, uvula deviation, muffled voice"),
("S", "Simple hypertrophy", "Smooth, uninfected enlargement"),
],
bg=LIGHT_PURPLE, title_color=PURPLE
))
e.append(sp(3))
e.append(warn_box("Unilateral tonsillar enlargement = malignancy until proven otherwise. "
"Refer for biopsy if no obvious infective cause."))
e.append(sp(3))
e.append(Paragraph("Tonsil Grading (Brodsky Scale)", H2))
e.append(simple_table(
["Grade", "Description"],
[
["1+", "Tonsils within tonsillar pillars"],
["2+", "Tonsils extend to pillars"],
["3+", "Tonsils extend beyond pillars but do not meet midline"],
["4+", "Tonsils meet midline ('kissing tonsils')"],
],
col_widths=[20*mm, PAGE_W-2*MARGIN-20*mm]
))
e.append(sp(3))
e.append(Paragraph("Laryngeal Examination", H2))
larynx_data = [
("Indirect laryngoscopy", "Patient leans forward, tongue protruded on gauze; warm mirror to prevent fogging; patient says 'eeee'"),
("Flexible nasopharyngolaryngoscopy", "Gold standard; passed transnasally after decongestion + topical anaesthesia"),
("What to assess", "Epiglottis, aryepiglottic folds, arytenoids, false cords, true cords, subglottis, cord mobility (abduct on inspiration, adduct on phonation)"),
("Voice changes", "Hoarseness: mucosal lesion or cord palsy; Muffled: supraglottic; Stridor: subglottic / tracheal"),
]
for title, body in larynx_data:
e.append(Paragraph(title, H3))
e.append(Paragraph(body, BODY))
e.append(sp(2))
e.append(mnemonic_box("HASTE (causes of hoarseness)",
[
("H", "Hypothyroidism", "Myxoedema voice - low, husky"),
("A", "Allergy / Laryngitis", "Acute or chronic inflammatory"),
("S", "Squamous cell carcinoma", "Persistent hoarseness > 3 weeks = urgent referral"),
("T", "Trauma / Thyroid surgery", "Recurrent laryngeal nerve palsy"),
("E", "Ectopic lesion (mediastinal)", "Aortic aneurysm, lung cancer compressing RLN"),
]
))
e.append(PageBreak())
return e
# ── SECTION 5 - NECK ─────────────────────────────────────────────────────────
def build_neck():
e = []
e.append(section_header("5. NECK EXAMINATION", color=GREEN))
e.append(sp(3))
e.append(Paragraph("Inspection", H2))
for item in [
"Scars (previous surgery, thyroidectomy)",
"Asymmetry or obvious masses",
"Tracheal deviation (mediastinal/thyroid mass)",
"Thyroid enlargement (goitre) - ask patient to swallow",
"Skin changes over mass (redness, tethering, punctum)",
]:
e.append(bullet(item))
e.append(sp(3))
e.append(Paragraph("Neck Levels (Robbins Classification) - MUST KNOW", H2))
e.append(simple_table(
["Level", "Region", "Boundaries", "Drainage From"],
[
["Ia", "Submental", "Between anterior digastric bellies", "Floor of mouth, anterior tongue, lower lip"],
["Ib", "Submandibular", "Submandibular triangle", "Oral cavity, submandibular gland"],
["II", "Upper jugular", "Skull base to hyoid (angle of mandible)", "Nasopharynx, oropharynx, parotid, oral cavity"],
["III", "Mid jugular", "Hyoid to cricoid", "Oral cavity, oropharynx, hypopharynx"],
["IV", "Lower jugular", "Cricoid to clavicle", "Hypopharynx, larynx, thyroid, oesophagus"],
["V", "Posterior triangle", "Between SCM, trapezius, clavicle", "Scalp, nasopharynx, thyroid"],
["VI", "Central compartment", "Between carotid sheaths, hyoid to sternal notch", "Thyroid, larynx, trachea, oesophagus"],
],
col_widths=[12*mm, 35*mm, 55*mm, PAGE_W-2*MARGIN-102*mm]
))
e.append(sp(3))
e.append(mnemonic_box("SIX (neck levels in order)",
[
("I", "Sub-chin + Sub-mand", "Level I = innermost (submental + submandibular)"),
("II", "Upper Jugular", "II = above hyoid"),
("III", "Mid Jugular", "III = hyoid to cricoid"),
("IV", "Lower Jugular", "IV = cricoid to clavicle"),
("V", "Posterior Triangle", "V = behind SCM"),
("VI", "Central", "VI = between carotid sheaths (thyroid territory)"),
],
bg=LIGHT_GREEN, title_color=GREEN
))
e.append(sp(3))
e.append(Paragraph("Palpation of a Neck Mass - What to Note", H2))
mass_chars = [
("Site / Level", "Which neck level? Midline vs. lateral?"),
("Size", "In cm - two dimensions"),
("Shape", "Rounded, irregular, lobulated"),
("Consistency", "Soft, firm, hard, rubbery (lymphoma = rubbery)"),
("Mobility", "Freely mobile, tethered to skin, fixed to deep structures"),
("Tenderness", "Tender = usually inflammatory; non-tender = ?malignant"),
("Pulsatility", "Carotid body tumour - transmitted pulsation, compressible"),
("Transilluminability", "Cystic hygroma / branchial cyst - transilluminate"),
("Tracheal deviation", "Push or pull effect from large mass"),
("Moves on swallowing", "Thyroid / thyroglossal cyst = moves up with swallow"),
("Moves on tongue protrusion", "Thyroglossal cyst ONLY = moves on tongue protrusion"),
]
e.append(simple_table(
["Characteristic", "Significance"],
mass_chars,
col_widths=[50*mm, PAGE_W-2*MARGIN-50*mm]
))
e.append(sp(3))
e.append(tip_box("Thyroglossal cyst: moves on BOTH swallowing AND tongue protrusion "
"(attached to foramen caecum via thyroglossal tract). "
"This distinguishes it from all other midline neck masses."))
e.append(PageBreak())
return e
# ── SECTION 6 - CRANIAL NERVES ───────────────────────────────────────────────
def build_cranial_nerves():
e = []
e.append(section_header("6. CRANIAL NERVE QUICK-CHECK (ENT-Relevant)"))
e.append(sp(3))
e.append(simple_table(
["CN", "Name", "ENT Test", "Finding if Affected"],
[
["I", "Olfactory", "Identify common smells unilaterally (coffee, vanilla)", "Anosmia: head injury, cribriform plate fracture, ethmoid tumour"],
["V", "Trigeminal", "Facial sensation V1/V2/V3; corneal reflex (V1 afferent)", "Reduced sensation: skull base tumour, trigeminal neuralgia"],
["VII", "Facial", "Raise eyebrows, close eyes tightly, show teeth, puff cheeks", "UMN: lower face only. LMN (Bell's palsy): ALL muscles ipsilateral"],
["VIII", "Vestibulo-cochlear", "Tuning forks + audiometry + Romberg + Unterberger", "Hearing loss (cochlear or retrocochlear); vestibular imbalance"],
["IX", "Glossopharyngeal", "Gag reflex (sensory limb), taste posterior 1/3 tongue", "Loss = unilateral palatal palsy, gag loss"],
["X", "Vagus", "Palate elevation on saying 'aah' - uvula deviates away from lesion", "Palate drags to normal side; vocal cord palsy (hoarseness)"],
["XI", "Accessory", "Shrug shoulders (trapezius), turn head against resistance (SCM)", "Shoulder drop; weakness turning head contralateral"],
["XII", "Hypoglossal", "Protrude tongue - deviates toward weak side (LMN lesion)", "Tongue deviation + wasting = LMN lesion (e.g., skull base metastasis)"],
],
col_widths=[10*mm, 30*mm, 65*mm, PAGE_W-2*MARGIN-105*mm]
))
e.append(sp(3))
e.append(mnemonic_box("BELL'S PALSY vs STROKE (LMN vs UMN VII palsy)",
[
("B", "Bell's = Both upper AND lower face", "LMN: forehead involved (cannot raise eyebrow)"),
("S", "Stroke = Spares forehead", "UMN: forehead SPARED (bilateral cortical supply to upper face)"),
("E", "Eye closure affected in Bell's", "Risk of exposure keratopathy - tape eye shut at night"),
("L", "LMN Bell's = taste loss (chorda tympani)", "Abnormal taste, hyperacusis (stapedius branch)"),
],
bg=LIGHT_RED, title_color=RED
))
e.append(sp(3))
e.append(Paragraph("Vestibular Assessment", H2))
e.append(simple_table(
["Test", "Method", "Positive Finding", "Interpretation"],
[
["Romberg", "Stand feet together, arms crossed, close eyes", "Increased sway / falls", "Peripheral vestibular or proprioceptive lesion"],
["Unterberger\n(Fukuda)", "March 50 steps on the spot, eyes closed", "Rotation > 45° toward one side", "Rotates toward side of vestibular lesion"],
["Dix-Hallpike", "Rapidly move from sitting to head-hanging position, 30-45° to one side", "Upbeat-torsional nystagmus (5-20s latency, fatigable)", "BPPV of posterior semicircular canal"],
["Head Impulse\n(HIT/HINTS)", "Rapid head thrust to one side; patient fixates on nose", "Catch-up saccade after thrust", "Peripheral vestibular lesion (ipsilateral)"],
["Hallpike-Dix\n(Negative)", "No nystagmus after Dix-Hallpike", "No response but severe vertigo", "Central cause - urgent MRI"],
],
col_widths=[25*mm, 50*mm, 45*mm, PAGE_W-2*MARGIN-120*mm]
))
e.append(sp(3))
e.append(tip_box("HINTS exam in acute vestibular syndrome: "
"Head Impulse (abnormal) + Nystagmus (unidirectional horizontal) + "
"Test of Skew (no skew deviation) = peripheral (benign). "
"A NORMAL head impulse test in acute vertigo suggests STROKE."))
e.append(PageBreak())
return e
# ── SECTION 7 - MEGA MNEMONICS ───────────────────────────────────────────────
def build_mnemonics():
e = []
e.append(section_header("7. HIGH-YIELD MNEMONICS COLLECTION"))
e.append(sp(3))
# Row 1
e.append(Paragraph("Ear Mnemonics", H2))
e.append(mnemonic_box("LEMON (risk factors for difficult airway - pre-op ENT)",
[
("L", "Look externally", "Beard, obesity, trauma, small mouth"),
("E", "Evaluate 3-3-2 rule", "Mouth opening 3F, thyromental 3F, thyroid-to-floor 2F"),
("M", "Mallampati class", "Class I-IV (higher = harder)"),
("O", "Obstruction", "Epiglottitis, peritonsillar abscess, tracheal deviation"),
("N", "Neck mobility", "Reduced in ankylosing spondylitis, cervical spine injury"),
]
))
e.append(sp(2))
e.append(mnemonic_box("SCOLD (features of chronic suppurative otitis media - safe type)",
[
("S", "Safe / mucosal type", "Central perforation, not dangerous"),
("C", "Central perforation", "Does NOT involve the annulus"),
("O", "Otorrhoea", "Mucopurulent, non-offensive discharge"),
("L", "Low risk of complications", "Intracranial complications rare"),
("D", "Dry ear possible", "Heals between episodes"),
],
bg=LIGHT_TEAL, title_color=MID_TEAL
))
e.append(sp(3))
e.append(Paragraph("Nose / Sinus Mnemonics", H2))
e.append(mnemonic_box("FADES (contents of the ostiomeatal complex)",
[
("F", "Frontal sinus ostium", "Drains into frontal recess / hiatus semilunaris"),
("A", "Anterior ethmoid sinuses", "Multiple cells draining into middle meatus"),
("D", "Drainage pathway", "All converge in the ostiomeatal complex (OMC)"),
("E", "Ethmoid infundibulum", "Groove in lateral nasal wall accepting maxillary ostium"),
("S", "Sphenoid does NOT", "Sphenoid drains into sphenoethmoidal recess (NOT OMC)"),
],
bg=LIGHT_ORANGE, title_color=ORANGE
))
e.append(sp(2))
e.append(mnemonic_box("SAMTER (Samter's triad = aspirin-exacerbated respiratory disease)",
[
("S", "Sinusitis (CRS with polyps)", "Chronic severe nasal polyps"),
("A", "Aspirin sensitivity", "NSAID-induced bronchospasm / rhinitis"),
("M/T", "asThMa", "Often severe / steroid-dependent"),
("E", "Eosinophilia", "High tissue and blood eosinophils"),
("R", "Recurrence after surgery", "Polyps regrow without NSAID avoidance"),
],
bg=LIGHT_ORANGE, title_color=ORANGE
))
e.append(sp(3))
e.append(Paragraph("Throat / Neck Mnemonics", H2))
e.append(mnemonic_box("MOVED (causes of unilateral recurrent laryngeal nerve palsy)",
[
("M", "Malignancy", "Lung (most common), thyroid, oesophageal, lymphoma"),
("O", "Operation", "Thyroidectomy, parathyroidectomy, cardiac surgery"),
("V", "Vascular", "Aortic aneurysm (left side), subclavian aneurysm (right)"),
("E", "Enlarged lymph nodes", "Lymphoma, metastatic nodes"),
("D", "Demyelination / idiopathic", "Multiple sclerosis, viral neuritis"),
],
bg=LIGHT_PURPLE, title_color=PURPLE
))
e.append(sp(2))
e.append(mnemonic_box("CATCH (features of neck mass suggesting malignancy)",
[
("C", "Consistency hard", "Rock hard = metastatic carcinoma"),
("A", "Age > 40", "New persistent neck mass in adult > 40 = cancer until proven otherwise"),
("T", "Tobacco + alcohol history", "Major risk factors for head and neck SCC"),
("C", "Can't move it", "Fixed to skin or deep structures = invasion"),
("H", "HPV history + tonsil asymmetry", "Oropharyngeal SCC (HPV16 p16+)"),
]
))
e.append(PageBreak())
return e
# ── SECTION 8 - EXAM TIPS ────────────────────────────────────────────────────
def build_exam_tips():
e = []
e.append(section_header("8. HIGH-YIELD EXAM TIPS"))
e.append(sp(3))
sections = [
("Ear Tips", MID_TEAL, [
"Always describe TM in 4 quadrants: antero-superior, antero-inferior, postero-superior, postero-inferior",
"Cholesteatoma is ALWAYS in the postero-superior quadrant or pars flaccida (attic) = 'unsafe' CSOM",
"Kernicterus and aminoglycosides (streptomycin, gentamicin) target hair cells of basal turn (high freq loss first)",
"Rinne false negative: severe sensorineural loss. Weber and audiogram will clarify",
"In BPPV: Dix-Hallpike positive side = the AFFECTED ear (nystagmus beats toward ground = geotropic = posterior canal BPPV)",
"Glomus jugulare: pulsatile tinnitus + conductive hearing loss + jugular foramen syndrome (CN IX, X, XI)",
"Presbycusis = bilateral symmetrical high-frequency sensorineural hearing loss in the elderly",
]),
("Nose Tips", ORANGE, [
"Anterior epistaxis: Kiesselbach's (Little's) plexus - 4 arteries meet here (sphenopalatine, greater palatine, anterior ethmoidal, superior labial)",
"Posterior epistaxis: sphenopalatine artery (branch of maxillary artery) - harder to control, may need surgery",
"CSF rhinorrhoea: halo test / beta-2 transferrin. High-res CT head + MRI for localisation",
"Nasal polyps in a child = cystic fibrosis until proven otherwise (sweat chloride test)",
"Inverted papilloma: unilateral, bleeds on touch, men > 50, risk of malignant transformation (~10%)",
"Wegener's (GPA): saddle nose deformity + nasal crusting + septal perforation + renal disease (c-ANCA positive)",
]),
("Throat Tips", PURPLE, [
"Ludwig's angina: bilateral submandibular space infection, airway emergency (floor of mouth elevation)",
"Quinsy (peritonsillar abscess): uvula deviates AWAY from the affected side; trismus present",
"Voice quality clues: hoarse = glottic pathology; muffled 'hot potato' voice = supraglottic; stridor = subglottic/tracheal",
"3-week rule: hoarseness > 3 weeks in a smoker/drinker = urgent laryngoscopy",
"In dysphagia: high dysphagia (level of throat/neck) suggests pharyngeal or cricopharyngeal pathology; progressive = ?malignancy",
"Plummer-Vinson syndrome: post-cricoid dysphagia + iron-deficiency anaemia + post-cricoid web (predisposing to carcinoma)",
]),
("Neck Tips", GREEN, [
"Rule of 80s for parotid masses: 80% parotid tumours are benign; 80% benign = pleomorphic adenoma; 80% pleomorphic = superficial lobe",
"Pleomorphic adenoma: slow-growing, painless, firm, lobulated; risk of malignant transformation if left untreated",
"Warthin's tumour (papillary cystadenoma lymphomatosum): older males, smokers, bilateral parotid, hot on Tc-99m scan",
"Sialadenitis vs sialolithiasis: pain + swelling on eating (calculus) vs constant swelling + pus from duct (infection)",
"Branchial cyst: anterior to upper 1/3 SCM, young adult, soft, transilluminable, contains cholesterol crystals",
"Cystic hygroma: multilocular, posterior triangle, infant, trans-illuminates brilliantly",
]),
]
for title, color, tips in sections:
e.append(Paragraph(title, H2))
for tip in tips:
e.append(bullet(tip))
e.append(sp(3))
e.append(PageBreak())
return e
# ── SECTION 9 - QUICK REFERENCE CARD ────────────────────────────────────────
def build_quick_ref():
e = []
e.append(section_header("9. QUICK REFERENCE SUMMARY CARD"))
e.append(sp(3))
e.append(Paragraph("ENT Examination Sequence at a Glance", H2))
order_data = [
["Step", "Action", "Key Points"],
["1", "General Appearance", "Vitals, gait, alertness, tobacco signs"],
["2", "Head & Facies", "Shape, symmetry, facial nerve function, skin lesions"],
["3", "External Ear", "Auricle, tragus, mastoid tenderness"],
["4", "Otoscopy", "Canal → TM landmarks → TM pathology"],
["5", "Pneumatic Otoscopy", "TM mobility; effusion or perforation = immobile"],
["6", "Tuning Forks", "Weber 512 Hz → Rinne 512 Hz → ABC if needed"],
["7", "External Nose", "Deviation, skin, nasal valve"],
["8", "Anterior Rhinoscopy", "Septum, inferior turbinate, mucosa, discharge"],
["9", "Nasal Endoscopy", "3-pass protocol; OMC, polyps, nasopharynx"],
["10", "Oral Cavity", "Lips → mucosa → floor → tongue → palate → tonsils → pharynx"],
["11", "Larynx", "Indirect mirror or flexible scope; cord mobility"],
["12", "Neck Inspection", "Scars, trachea, thyroid; ask to swallow"],
["13", "Neck Palpation", "Levels I-VI; thyroid; parotid; submandibular"],
["14", "Cranial Nerves", "I, V, VII, VIII, IX, X, XI, XII"],
["15", "Vestibular Tests", "Romberg, Unterberger, Dix-Hallpike, HIT as indicated"],
]
step_tbl2 = Table(order_data, colWidths=[12*mm, 50*mm, PAGE_W-2*MARGIN-62*mm])
step_tbl2.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), DARK_TEAL),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_TEAL]),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
("FONTSIZE", (0,0), (-1,-1), 8.5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING",(0,0),(-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING",(0,0), (-1,-1), 6),
("ALIGN", (0,0), (0,-1), "CENTER"),
("FONTNAME", (0,1), (0,-1), "Helvetica-Bold"),
("TEXTCOLOR", (0,1), (0,-1), DARK_TEAL),
]))
e.append(step_tbl2)
e.append(sp(4))
# Final tip strip
final_data = [[
Paragraph(
"<b>Remember:</b> The ENT examination flows HEAD to TOE within the head - "
"always Ear first, then Nose, then Throat, then Neck, then Cranial Nerves. "
"This systematic approach prevents missing pathology and impresses examiners.",
S("FT", fontName="Helvetica", fontSize=9.5,
textColor=DARK_TEAL, alignment=TA_CENTER, leading=14))
]]
final_tbl = Table(final_data, colWidths=[PAGE_W - 2*MARGIN])
final_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_TEAL),
("LINEABOVE", (0,0), (-1, 0), 3, DARK_TEAL),
("LINEBELOW", (0,-1), (-1,-1), 3, DARK_TEAL),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING",(0,0),(-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 14),
("RIGHTPADDING",(0,0), (-1,-1), 14),
]))
e.append(final_tbl)
e.append(sp(3))
e.append(Paragraph("Source: Cummings Otolaryngology: Head and Neck Surgery (Cummings et al.)",
CENTER_SM))
return e
# ── PAGE TEMPLATE ─────────────────────────────────────────────────────────────
def on_page(canvas, doc):
canvas.saveState()
w, h = A4
# Footer
canvas.setFillColor(colors.HexColor("#EEEEEE"))
canvas.rect(0, 0, w, 14*mm, fill=1, stroke=0)
canvas.setFillColor(DARK_TEAL)
canvas.setFont("Helvetica-Bold", 8)
canvas.drawCentredString(w/2, 5*mm, "ENT Examination Study Guide • Based on Cummings Otolaryngology")
canvas.setFillColor(colors.HexColor("#888888"))
canvas.setFont("Helvetica", 7)
canvas.drawRightString(w - MARGIN, 5*mm, f"Page {doc.page}")
# Top rule
canvas.setStrokeColor(DARK_TEAL)
canvas.setLineWidth(1.5)
canvas.line(MARGIN, h - 12*mm, w - MARGIN, h - 12*mm)
canvas.restoreState()
# ── BUILD ─────────────────────────────────────────────────────────────────────
def build_pdf(out_path):
doc = SimpleDocTemplate(
out_path,
pagesize=A4,
leftMargin=MARGIN,
rightMargin=MARGIN,
topMargin=16*mm,
bottomMargin=18*mm,
title="ENT Examination Study Guide",
author="Orris Medical AI",
subject="Otolaryngology Exam Preparation",
)
story = []
story += build_cover()
story += build_history()
story += build_ear()
story += build_nose()
story += build_throat()
story += build_neck()
story += build_cranial_nerves()
story += build_mnemonics()
story += build_exam_tips()
story += build_quick_ref()
doc.build(story, onFirstPage=on_page, onLaterPages=on_page)
print(f"PDF generated: {out_path}")
if __name__ == "__main__":
build_pdf("/home/daytona/workspace/ent-study-guide/ENT_Examination_Study_Guide.pdf")
cd /home/daytona/workspace/ent-study-guide && python generate_ent_guide.py 2>&1
ls -lh /home/daytona/workspace/ent-study-guide/ENT_Examination_Study_Guide.pdf
ENT Examination Study Guide
PDF Document · PDF
| Section | Contents |
|---|---|
| Cover Page | Color-coded overview with Ear / Nose / Throat summary panels |
| 1. History Taking | SOCRATES+ framework, risk factor domains, history mnemonic |
| 2. Ear Examination | Step-by-step otoscopy, TM landmarks, TM findings table, tuning fork test table, WEBER RULE mnemonic, false-negative Rinne warning |
| 3. Nose Examination | 3-pass endoscopy protocol, rhinoscopy tips, mucosa colour guide, SEPTUM mnemonic (causes of septal perforation) |
| 4. Throat Examination | Oral cavity structures checklist, Brodsky tonsil grading scale, LOTS mnemonic (tonsillar enlargement), laryngeal assessment guide, HASTE mnemonic (hoarseness) |
| 5. Neck Examination | Neck mass characteristics, Robbins Levels I-VI table, thyroglossal vs branchial cyst tips, CATCH mnemonic (malignancy features) |
| 6. Cranial Nerve Quick-Check | CN I, V, VII, VIII, IX, X, XI, XII in one table; BELL'S PALSY vs STROKE mnemonic; vestibular tests (Dix-Hallpike, HIT/HINTS) |
| 7. Mnemonics Collection | LEMON (airway), SCOLD (safe CSOM), FADES (OMC), SAMTER, MOVED (RLN palsy) |
| 8. High-Yield Exam Tips | ~28 clinical pearls across Ear, Nose, Throat, Neck |
| 9. Quick Reference Card | 15-step examination sequence at a glance |