Contact granuloma
"contact granuloma" larynx vocal cord

| Cause | Notes |
|---|---|
| Vocal abuse / misuse | Forceful glottal stroke, habitual low pitch, hard attack |
| Throat clearing / chronic coughing | Repetitive traumatic apposition of arytenoids |
| Laryngopharyngeal reflux (LPR) / GERD | Acid inflames the vulnerable posterior laryngeal mucosa |
| Endotracheal intubation | Pressure necrosis from the ETT tip on the vocal process |
| Glottal incompetence | Vocal fold atrophy causes compensatory hyperfunction |
Convert to a pdf format
ls /home/daytona/workspace/ 2>/dev/null || echo "workspace empty or missing"
mkdir -p /home/daytona/workspace/contact-granuloma && python3 -c "from reportlab…
~/contact-granuloma/generate_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, ListFlowable, ListItem, KeepTogether
)
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.platypus import PageBreak
import io, urllib.request
OUTPUT = "/home/daytona/workspace/contact-granuloma/Contact_Granuloma.pdf"
# ── Styles ─────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
BRAND_BLUE = colors.HexColor("#1a4a7a")
BRAND_TEAL = colors.HexColor("#0f7b8c")
LIGHT_BLUE = colors.HexColor("#e8f4f8")
LIGHT_GRAY = colors.HexColor("#f5f5f5")
MED_GRAY = colors.HexColor("#555555")
TABLE_HEADER = colors.HexColor("#1a4a7a")
TABLE_ALT = colors.HexColor("#eaf4fb")
title_style = ParagraphStyle(
"DocTitle", parent=styles["Title"],
fontSize=22, textColor=colors.white,
spaceAfter=4, leading=28, alignment=TA_LEFT,
fontName="Helvetica-Bold"
)
subtitle_style = ParagraphStyle(
"DocSubtitle", parent=styles["Normal"],
fontSize=11, textColor=colors.HexColor("#d0eaf5"),
spaceAfter=0, leading=14, alignment=TA_LEFT,
fontName="Helvetica"
)
h1_style = ParagraphStyle(
"H1", parent=styles["Heading1"],
fontSize=14, textColor=BRAND_BLUE,
spaceBefore=14, spaceAfter=4, leading=18,
fontName="Helvetica-Bold", borderPad=2,
borderColor=BRAND_TEAL, borderWidth=0,
)
h2_style = ParagraphStyle(
"H2", parent=styles["Heading2"],
fontSize=11, textColor=BRAND_TEAL,
spaceBefore=10, spaceAfter=3, leading=14,
fontName="Helvetica-Bold",
)
body_style = ParagraphStyle(
"Body", parent=styles["Normal"],
fontSize=10, textColor=colors.HexColor("#222222"),
spaceAfter=5, leading=15, alignment=TA_JUSTIFY,
fontName="Helvetica"
)
bullet_style = ParagraphStyle(
"Bullet", parent=body_style,
leftIndent=14, spaceAfter=3,
)
caption_style = ParagraphStyle(
"Caption", parent=styles["Normal"],
fontSize=8.5, textColor=MED_GRAY,
spaceAfter=8, leading=12, alignment=TA_CENTER,
fontName="Helvetica-Oblique"
)
source_style = ParagraphStyle(
"Source", parent=styles["Normal"],
fontSize=8, textColor=MED_GRAY,
spaceAfter=3, leading=11,
fontName="Helvetica-Oblique"
)
keypoint_style = ParagraphStyle(
"KeyPoint", parent=body_style,
leftIndent=10, textColor=BRAND_BLUE,
fontName="Helvetica-Bold", fontSize=10,
)
def hr():
return HRFlowable(width="100%", thickness=1, color=BRAND_TEAL, spaceAfter=6, spaceBefore=4)
def h1(text):
return Paragraph(text, h1_style)
def h2(text):
return Paragraph(text, h2_style)
def body(text):
return Paragraph(text, body_style)
def bullet(items):
elems = []
for item in items:
elems.append(ListItem(Paragraph(item, bullet_style), bulletColor=BRAND_TEAL, value='bullet'))
return ListFlowable(elems, bulletType='bullet', leftIndent=18, bulletFontSize=8)
def spacer(h=6):
return Spacer(1, h)
# ── Document ────────────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=2.2*cm, rightMargin=2.2*cm,
topMargin=2*cm, bottomMargin=2*cm,
title="Contact Granuloma",
author="Orris Medical Library",
subject="Otolaryngology – Vocal Fold Granuloma"
)
story = []
# ── Header Banner ────────────────────────────────────────────────────────────
banner_data = [[
Paragraph("Contact Granuloma", title_style),
]]
banner_table = Table(banner_data, colWidths=[doc.width])
banner_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), BRAND_BLUE),
("TOPPADDING", (0,0), (-1,-1), 16),
("BOTTOMPADDING", (0,0), (-1,-1), 12),
("LEFTPADDING", (0,0), (-1,-1), 14),
("RIGHTPADDING", (0,0), (-1,-1), 14),
("ROUNDEDCORNERS", [6]),
]))
story.append(banner_table)
subtitle_data = [[
Paragraph("Vocal Fold Granuloma / Contact Ulcer | Otolaryngology Reference", subtitle_style)
]]
sub_table = Table(subtitle_data, colWidths=[doc.width])
sub_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), BRAND_TEAL),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 14),
("RIGHTPADDING", (0,0), (-1,-1), 14),
]))
story.append(sub_table)
story.append(spacer(12))
# ── Definition ───────────────────────────────────────────────────────────────
story.append(h1("Definition"))
story.append(hr())
story.append(body(
"A contact granuloma is a reactive, inflammatory lesion arising on the <b>vocal process of the "
"arytenoid cartilage</b> — the posterior, cartilaginous part of the glottis. When the lesion "
"presents as an excavation rather than a heaped-up mass it is termed a <b>contact ulcer</b>. "
"The two forms represent a spectrum of the same pathological process."
))
story.append(spacer(4))
# ── Epidemiology ─────────────────────────────────────────────────────────────
story.append(h1("Epidemiology"))
story.append(hr())
story.append(bullet([
"Predominantly affects <b>men</b>",
"Associated with vocally demanding occupations and hard glottal attack speakers",
"Chronic throat clearers and acid reflux patients are at elevated risk",
"Psychological stress has also been implicated as a contributing factor",
]))
story.append(spacer(4))
# ── Etiology ─────────────────────────────────────────────────────────────────
story.append(h1("Causes / Etiology"))
story.append(hr())
etio_data = [
["Cause", "Notes"],
["Vocal abuse / misuse", "Forceful glottal stroke, habitual low pitch, hard attack"],
["Throat clearing / chronic coughing", "Repetitive traumatic apposition of arytenoids"],
["LPR / GERD", "Acid inflames the vulnerable posterior laryngeal mucosa"],
["Endotracheal intubation", "Pressure necrosis from ETT tip on the vocal process"],
["Glottal incompetence", "Vocal fold atrophy causes compensatory hyperfunction"],
]
etio_table = Table(etio_data, colWidths=[6.5*cm, doc.width - 6.5*cm])
etio_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("TEXTCOLOR", (0,0), (-1,0), colors.white),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 9.5),
("BOTTOMPADDING", (0,0), (-1,0), 7),
("TOPPADDING", (0,0), (-1,0), 7),
("ROWBACKGROUNDS",(0,1), (-1,-1), [colors.white, TABLE_ALT]),
("ALIGN", (0,0), (-1,-1), "LEFT"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,1), (-1,-1), 5),
("BOTTOMPADDING", (0,1), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#cccccc")),
("ROUNDEDCORNERS",[4]),
]))
story.append(etio_table)
story.append(spacer(8))
# ── Pathophysiology ───────────────────────────────────────────────────────────
story.append(h1("Pathophysiology"))
story.append(hr())
story.append(body(
"The thin mucosa and perichondrium overlying the <b>cartilaginous glottis</b> are vulnerable to:"
))
story.append(bullet([
"Overly forceful apposition (slamming) of the arytenoids at glottal onset",
"Chronic coughing and repetitive throat clearing",
"Acid-mediated inflammation of the vocal process region via LPR",
]))
story.append(body(
"The traumatized area either <b>ulcerates</b> (contact ulcer) or forms a <b>heaped-up granuloma</b> "
"through reparative granulation tissue formation. The characteristic <b>bilobed morphology</b> arises "
"because the contralateral arytenoid contacts the lesion at its midpoint during phonation — at closure, "
"the opposing vocal process fits exactly into the cleft between the two lobes."
))
story.append(spacer(4))
# ── Clinical Features ─────────────────────────────────────────────────────────
story.append(h1("Clinical Features"))
story.append(hr())
story.append(h2("Symptoms"))
story.append(bullet([
"Unilateral discomfort over the midthyroid cartilage area",
"<b>Referred otalgia</b> to the ipsilateral ear (via Arnold's nerve, a branch of CN X) — characteristic",
"Foreign body sensation / globus pharyngeus",
"Hoarseness (only when the lesion is large)",
"Frequent throat clearing",
"History of intubation, chronic reflux, or heavy voice use",
]))
story.append(spacer(4))
story.append(h2("Voice Characteristics"))
story.append(bullet([
"May sound normal or only slightly husky",
"Habitual use of an overly <b>low fundamental frequency</b>",
"Held-back, constrained vocal quality",
"Low, monotone speech pattern",
]))
story.append(spacer(8))
# ── Laryngeal Examination ─────────────────────────────────────────────────────
story.append(h1("Laryngeal Examination"))
story.append(hr())
story.append(bullet([
"Depressed, ulcerated area with whitish exudate <b>or</b> a bilobed, heaped-up lesion at the <b>vocal process of the arytenoid</b>",
"Erythema on the vocal process extending up the medial arytenoid surface",
"At phonatory closure, the contralateral vocal process fits into the granuloma cleft",
"Maturing granuloma becomes <b>pedunculated</b> and may flip above/below the vocal fold plane",
"Detection of early ulcers may require a rigid telescope or distal-chip camera",
"<b>Stroboscopy</b> can reveal glottal incompetency as a contributing factor",
]))
story.append(spacer(8))
# ── Management ────────────────────────────────────────────────────────────────
story.append(h1("Management"))
story.append(hr())
mgmt_data = [
["Step", "Treatment", "Details"],
["1st line", "Anti-reflux therapy",
"Empiric PPIs even without overt GERD symptoms. Lifestyle changes: avoid caffeine, alcohol, late-night eating."],
["1st line", "Voice therapy",
"Vocal hygiene, abolish throat clearing, raise average speaking pitch, reduce hard glottal attack."],
["2nd line", "Corticosteroid injection",
"Depot steroid (triamcinolone) injected into lesion + base via videoendoscopy. Inhaled triamcinolone or mometasone also used. Re-assess at 6-8 week intervals."],
["Refractory", "Botulinum toxin",
"Inject TA-LCA muscle complex to reduce forceful arytenoid apposition. Causes significant temporary dysphonia — counsel patient."],
["Last resort", "Surgery",
"Only for lesions not responding to medical therapy or if malignancy must be excluded. Wait for spontaneous maturation (3-6 months). Leave base/pedicle undisturbed. High recurrence rate."],
]
col_w = [2.5*cm, 3.8*cm, doc.width - 6.3*cm]
mgmt_table = Table(mgmt_data, colWidths=col_w)
mgmt_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("TEXTCOLOR", (0,0), (-1,0), colors.white),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 9),
("BOTTOMPADDING", (0,0), (-1,0), 7),
("TOPPADDING", (0,0), (-1,0), 7),
("ROWBACKGROUNDS",(0,1), (-1,-1), [colors.white, TABLE_ALT]),
("ALIGN", (0,0), (-1,-1), "LEFT"),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,1), (-1,-1), 5),
("BOTTOMPADDING", (0,1), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 7),
("RIGHTPADDING", (0,0), (-1,-1), 7),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#cccccc")),
]))
story.append(mgmt_table)
story.append(spacer(10))
# ── Intubation Granuloma ──────────────────────────────────────────────────────
story.append(h1("Intubation Granuloma (Special Subtype)"))
story.append(hr())
story.append(bullet([
"More common in <b>females</b> (unlike phonotraumatic granuloma, which favors males)",
"Occurs after brief or prolonged endotracheal intubation",
"Mechanism: pressure necrosis from the ETT at the posterior commissure and vocal process",
"Natural history: often matures and spontaneously detaches without intervention",
'Characteristic "divots" at the posterior commissure may become visible after detachment',
]))
story.append(spacer(8))
# ── Key Points ────────────────────────────────────────────────────────────────
story.append(h1("Key Points"))
story.append(hr())
keys = [
("Location:", "Always at the vocal process of the arytenoid (posterior, cartilaginous glottis) — NOT the membranous vocal fold."),
("Spectrum:", "Contact ulcer = excavated form; granuloma = heaped-up reparative form. They are the same entity."),
("Otalgia:", "Referred pain to the ipsilateral ear is characteristic (Arnold's nerve, branch of CN X)."),
("Natural history:", "Spontaneous resolution is the rule with medical management (3-6 months)."),
("Surgery:", "Last resort due to very high recurrence rate."),
("Botulinum toxin:", "Important tool for refractory or recurrent cases — reduces forceful arytenoid apposition."),
("Intubation type:", "More common in females; phonotraumatic type more common in males."),
]
kp_data = [[Paragraph(f"<b>{k}</b>", body_style), Paragraph(v, body_style)] for k, v in keys]
kp_table = Table(kp_data, colWidths=[4*cm, doc.width - 4*cm])
kp_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_BLUE),
("ALIGN", (0,0), (-1,-1), "LEFT"),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("LINEBELOW", (0,0), (-1,-2), 0.3, colors.HexColor("#b0c8d8")),
]))
story.append(kp_table)
story.append(spacer(14))
# ── Sources ───────────────────────────────────────────────────────────────────
story.append(hr())
story.append(Paragraph("<b>Sources</b>", source_style))
story.append(Paragraph(
"Cummings Otolaryngology Head and Neck Surgery, pp. 1125-1126",
source_style
))
story.append(Paragraph(
"K.J. Lee's Essential Otolaryngology, p. 923",
source_style
))
story.append(Paragraph(
"Teng TZJ et al. Vocal Fold Granuloma: Updates and Advancements in Treatment. J Voice. 2026 Mar. [PMID: 37985285]",
source_style
))
# ── Build ─────────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF written to: {OUTPUT}")
python3 /home/daytona/workspace/contact-granuloma/generate_pdf.py 2>&1
Contact Granuloma
PDF Document · PDF