Acid base fluid balance imbalance
pH = pKa + log([HCO3-] / [H2CO3])
| Parameter | Normal Range |
|---|---|
| pH | 7.35-7.45 |
| PaCO2 | 35-45 mmHg |
| HCO3- | 22-26 mEq/L |
| Base excess | -2 to +2 |

| Type | Anion Gap | Mechanism | Examples |
|---|---|---|---|
| High AG acidosis | >12 mEq/L | Unmeasured anions accumulate | Lactic acidosis, DKA, uremia, salicylates, methanol, ethylene glycol (mnemonic: A MUDPILE CAT) |
| Normal AG (hyperchloremic) acidosis | 8-12 mEq/L | Cl- replaces lost HCO3- | Diarrhea, renal tubular acidosis (RTA), saline infusion, urinary diversions |
AG = Na+ - (Cl- + HCO3-) -- Normal: 8-12 mEq/L
| Acute | Chronic | |
|---|---|---|
| HCO3- rise per 10 mmHg PCO2 increase | +1 mEq/L | +3-4 mEq/L |
| Acute | Chronic | |
|---|---|---|
| HCO3- fall per 10 mmHg PCO2 decrease | -2 mEq/L | -5 mEq/L |
| Primary Disorder | Expected Compensation |
|---|---|
| Metabolic acidosis | PCO2 = (1.5 × HCO3-) + 8 ± 2 OR ↓PCO2 = 1.3 × ↓HCO3- |
| Metabolic alkalosis | ↑PCO2 = 0.6 × ↑HCO3- |
| Respiratory acidosis (acute) | ↑HCO3- = 1 mEq/L per 10 mmHg ↑PCO2 |
| Respiratory acidosis (chronic) | ↑HCO3- = 3-4 mEq/L per 10 mmHg ↑PCO2 |
| Respiratory alkalosis (acute) | ↓HCO3- = 2 mEq/L per 10 mmHg ↓PCO2 |
| Respiratory alkalosis (chronic) | ↓HCO3- = 5 mEq/L per 10 mmHg ↓PCO2 |
| Mixed Disorder | Clinical Context |
|---|---|
| Metabolic acidosis + respiratory alkalosis | Salicylate toxicity, sepsis, hepatic failure |
| Metabolic alkalosis + respiratory acidosis | COPD with vomiting or diuretic use |
| Metabolic acidosis + metabolic alkalosis | Renal failure with vomiting |
| Mixed respiratory + metabolic acidosis | Cardiac arrest, severe COPD exacerbation |
| Fluid Imbalance | Associated Acid-Base Effect | Mechanism |
|---|---|---|
| Volume depletion | Metabolic alkalosis (contraction alkalosis) | HCO3- concentration rises as volume decreases; aldosterone increases H+ secretion |
| Normal saline infusion | Hyperchloremic (non-gap) metabolic acidosis | Excess Cl- dilutes HCO3- and displaces it |
| Diarrhea | Non-gap metabolic acidosis | Direct HCO3- loss in stool |
| Vomiting / NG suction | Metabolic alkalosis | HCl loss from stomach |
| Renal failure / oliguria | High AG metabolic acidosis | Retention of sulfates, phosphates, organic acids |
| Mannitol / hypotonic fluids | Dilutional (non-gap) metabolic acidosis | Dilution of serum HCO3- |
Create a quick reference PDF for ABG interpretation rules
ls -la /home/daytona/workspace/ 2>/dev/null || echo "workspace empty or not found"
mkdir -p /home/daytona/workspace/abg-reference
~/abg-reference/generate_abg_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.lib.colors import HexColor
OUTPUT = "/home/daytona/workspace/abg-reference/ABG_Quick_Reference.pdf"
# ── Colour palette ──────────────────────────────────────────────────────────
C_HEADER_BG = HexColor("#1A3A5C") # dark navy
C_HEADER_TEXT = HexColor("#FFFFFF")
C_ACID_BG = HexColor("#FFE5E5") # soft red – acidosis
C_ACID_HDR = HexColor("#C0392B")
C_ALK_BG = HexColor("#E5F0FF") # soft blue – alkalosis
C_ALK_HDR = HexColor("#1A6FAF")
C_NEUTRAL_BG = HexColor("#F5F5F0") # warm grey
C_SECTION_HDR = HexColor("#2C5F8A")
C_ROW_ALT = HexColor("#EFF4FA")
C_BORDER = HexColor("#B0C4D8")
C_YELLOW_BG = HexColor("#FFF9E6")
C_YELLOW_HDR = HexColor("#B8860B")
C_GREEN_HDR = HexColor("#1E7D4A")
C_GREEN_BG = HexColor("#E6F5EC")
C_TEXT = HexColor("#1C1C1C")
C_MUTED = HexColor("#555555")
# ── Document setup ───────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
topMargin=12*mm,
bottomMargin=12*mm,
leftMargin=14*mm,
rightMargin=14*mm,
title="ABG Quick Reference",
author="Orris Medical"
)
W = A4[0] - 28*mm # usable width
# ── Styles ────────────────────────────────────────────────────────────────────
base = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
sTitle = S("sTitle",
fontSize=20, leading=26, textColor=C_HEADER_TEXT, alignment=TA_CENTER,
fontName="Helvetica-Bold")
sSubtitle = S("sSubtitle",
fontSize=10, leading=14, textColor=HexColor("#C8DCF0"), alignment=TA_CENTER,
fontName="Helvetica")
sSectionHdr = S("sSectionHdr",
fontSize=9.5, leading=12, textColor=C_HEADER_TEXT, alignment=TA_LEFT,
fontName="Helvetica-Bold", leftIndent=4)
sBodySm = S("sBodySm",
fontSize=8, leading=11, textColor=C_TEXT, fontName="Helvetica")
sBodySmC = S("sBodySmC",
fontSize=8, leading=11, textColor=C_TEXT, fontName="Helvetica", alignment=TA_CENTER)
sBodyBold = S("sBodyBold",
fontSize=8, leading=11, textColor=C_TEXT, fontName="Helvetica-Bold")
sBodyBoldC = S("sBodyBoldC",
fontSize=8, leading=11, textColor=C_TEXT, fontName="Helvetica-Bold", alignment=TA_CENTER)
sTblHdr = S("sTblHdr",
fontSize=8, leading=10, textColor=C_HEADER_TEXT, fontName="Helvetica-Bold", alignment=TA_CENTER)
sTblCell = S("sTblCell",
fontSize=7.5, leading=10, textColor=C_TEXT, fontName="Helvetica")
sTblCellC = S("sTblCellC",
fontSize=7.5, leading=10, textColor=C_TEXT, fontName="Helvetica", alignment=TA_CENTER)
sTblCellB = S("sTblCellB",
fontSize=7.5, leading=10, textColor=C_TEXT, fontName="Helvetica-Bold")
sFormula = S("sFormula",
fontSize=7.5, leading=11, textColor=HexColor("#003366"), fontName="Helvetica-Bold")
sFooter = S("sFooter",
fontSize=6.5, leading=9, textColor=C_MUTED, fontName="Helvetica", alignment=TA_CENTER)
def cell(text, style=sTblCell, pad=2):
return Paragraph(text, style)
def hdr_cell(text):
return Paragraph(text, sTblHdr)
# ── Helper: section header band ───────────────────────────────────────────────
def section_band(title, bg=C_SECTION_HDR, full_width=True):
w = W if full_width else W
t = Table([[Paragraph(title, sSectionHdr)]], colWidths=[w])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("ROUNDEDCORNERS", [3, 3, 3, 3]),
]))
return t
def sp(h=4):
return Spacer(1, h)
# ════════════════════════════════════════════════════════════════════════════
story = []
# ── TITLE BANNER ─────────────────────────────────────────────────────────────
title_data = [[
Paragraph("ABG Quick Reference", sTitle),
]]
title_tbl = Table(title_data, colWidths=[W])
title_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_HEADER_BG),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
]))
story.append(title_tbl)
subtitle_data = [[
Paragraph("Arterial Blood Gas Interpretation | Compensation Rules | Anion Gap | Mixed Disorders", sSubtitle)
]]
subtitle_tbl = Table(subtitle_data, colWidths=[W])
subtitle_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_HEADER_BG),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
]))
story.append(subtitle_tbl)
story.append(sp(6))
# ── NORMAL VALUES ─────────────────────────────────────────────────────────────
story.append(section_band(" NORMAL ABG VALUES"))
story.append(sp(3))
nv_data = [
[hdr_cell("Parameter"), hdr_cell("Normal Range"), hdr_cell("Acidosis"), hdr_cell("Alkalosis")],
[cell("pH"), cell("7.35 – 7.45", sTblCellC), cell("< 7.35", sTblCellC), cell("> 7.45", sTblCellC)],
[cell("PaCO\u2082 (mmHg)"), cell("35 – 45", sTblCellC), cell("> 45", sTblCellC), cell("< 35", sTblCellC)],
[cell("HCO\u2083\u207b (mEq/L)"), cell("22 – 26", sTblCellC), cell("< 22", sTblCellC), cell("> 26", sTblCellC)],
[cell("Base Excess"), cell("-2 to +2", sTblCellC), cell("< -2", sTblCellC), cell("> +2", sTblCellC)],
[cell("PaO\u2082 (mmHg)"), cell("80 – 100", sTblCellC), cell("—", sTblCellC), cell("—", sTblCellC)],
[cell("SaO\u2082 (%)"), cell("> 95", sTblCellC), cell("—", sTblCellC), cell("—", sTblCellC)],
]
col_w = [W*0.28, W*0.24, W*0.24, W*0.24]
nv_tbl = Table(nv_data, colWidths=col_w, repeatRows=1)
nv_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_HEADER_BG),
("BACKGROUND", (0,1), (-1,-1), C_NEUTRAL_BG),
("ROWBACKGROUNDS",(0,1), (-1,-1), [colors.white, C_ROW_ALT]),
("GRID", (0,0), (-1,-1), 0.5, C_BORDER),
("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", (1,1), (-1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(nv_tbl)
story.append(sp(7))
# ── STEP-BY-STEP INTERPRETATION ───────────────────────────────────────────────
story.append(section_band(" STEP-BY-STEP ABG INTERPRETATION"))
story.append(sp(3))
steps_data = [
[cell("Step 1", sTblCellB), cell("Check pH", sTblCellB),
cell("pH < 7.35 = Acidosis | pH > 7.45 = Alkalosis | pH 7.35–7.45 = Normal / Compensated")],
[cell("Step 2", sTblCellB), cell("Identify primary\nprocess", sTblCellB),
cell("If acidosis: Is PaCO\u2082 high (resp. acidosis) OR HCO\u2083\u207b low (metabolic acidosis)?\n"
"If alkalosis: Is PaCO\u2082 low (resp. alkalosis) OR HCO\u2083\u207b high (metabolic alkalosis)?")],
[cell("Step 3", sTblCellB), cell("Check\ncompensation", sTblCellB),
cell("Calculate expected compensation (see table below). "
"If actual value \u2260 expected \u2192 mixed disorder present.")],
[cell("Step 4", sTblCellB), cell("Anion Gap\n(if met. acidosis)", sTblCellB),
cell("AG = Na\u207a \u2212 (Cl\u207b + HCO\u2083\u207b). Normal 8\u201312 mEq/L. "
"Elevated AG \u2192 high-AG acidosis (MUDPILE). Normal AG \u2192 hyperchloremic acidosis.")],
[cell("Step 5", sTblCellB), cell("Delta-Delta ratio\n(if high AG)", sTblCellB),
cell("(AG \u2212 12) \u00f7 (24 \u2212 HCO\u2083\u207b). "
"0.4–1: mixed high-AG + normal-AG acidosis. "
"1–2: pure high-AG acidosis. "
">2: high-AG acidosis + metabolic alkalosis.")],
]
step_col = [W*0.08, W*0.16, W*0.76]
steps_tbl = Table(steps_data, colWidths=step_col)
steps_tbl.setStyle(TableStyle([
("ROWBACKGROUNDS", (0,0), (-1,-1), [colors.white, C_ROW_ALT]),
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("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"),
("BACKGROUND", (0,0), (0,-1), HexColor("#D6E4F0")),
]))
story.append(steps_tbl)
story.append(sp(7))
# ── COMPENSATION RULES ────────────────────────────────────────────────────────
story.append(section_band(" COMPENSATION RULES"))
story.append(sp(3))
comp_data = [
[hdr_cell("Disorder"), hdr_cell("pH"), hdr_cell("1\u00b0 Change"), hdr_cell("Compensatory Response"),
hdr_cell("Expected Compensation Formula")],
# Metabolic acidosis
[Paragraph("<b>Metabolic<br/>Acidosis</b>", sTblCellC),
Paragraph("<font color='#C0392B'><b>\u2193 Low</b></font>", sTblCellC),
Paragraph("<b>\u2193 HCO\u2083\u207b</b>", sTblCellC),
cell("Lungs hyperventilate\n\u2193 PaCO\u2082", sTblCellC),
Paragraph("<b>PaCO\u2082 = (1.5 \u00d7 HCO\u2083\u207b) + 8 \u00b1 2</b><br/>"
"<i>Or: \u0394PaCO\u2082 = 1.3 \u00d7 \u0394HCO\u2083\u207b</i>", sFormula)],
# Metabolic alkalosis
[Paragraph("<b>Metabolic<br/>Alkalosis</b>", sTblCellC),
Paragraph("<font color='#1A6FAF'><b>\u2191 High</b></font>", sTblCellC),
Paragraph("<b>\u2191 HCO\u2083\u207b</b>", sTblCellC),
cell("Lungs hypoventilate\n\u2191 PaCO\u2082", sTblCellC),
Paragraph("<b>\u0394PaCO\u2082 = 0.6 \u00d7 \u0394HCO\u2083\u207b</b><br/>"
"<i>(PaCO\u2082 rarely exceeds 55\u201360 mmHg)</i>", sFormula)],
# Respiratory acidosis acute
[Paragraph("<b>Respiratory\nAcidosis</b>\n<i>(Acute)</i>", sTblCellC),
Paragraph("<font color='#C0392B'><b>\u2193 Low</b></font>", sTblCellC),
Paragraph("<b>\u2191 PaCO\u2082</b>", sTblCellC),
cell("Kidneys retain HCO\u2083\u207b\n(hours\u2013days)", sTblCellC),
Paragraph("<b>\u0394HCO\u2083\u207b = +1 mEq/L per 10 mmHg \u2191 PaCO\u2082</b>", sFormula)],
# Respiratory acidosis chronic
[Paragraph("<b>Respiratory\nAcidosis</b>\n<i>(Chronic)</i>", sTblCellC),
Paragraph("<font color='#C0392B'><b>\u2193 Low</b></font>", sTblCellC),
Paragraph("<b>\u2191 PaCO\u2082</b>", sTblCellC),
cell("Kidneys retain HCO\u2083\u207b\n(3\u20135 days full comp.)", sTblCellC),
Paragraph("<b>\u0394HCO\u2083\u207b = +3\u20134 mEq/L per 10 mmHg \u2191 PaCO\u2082</b>", sFormula)],
# Respiratory alkalosis acute
[Paragraph("<b>Respiratory\nAlkalosis</b>\n<i>(Acute)</i>", sTblCellC),
Paragraph("<font color='#1A6FAF'><b>\u2191 High</b></font>", sTblCellC),
Paragraph("<b>\u2193 PaCO\u2082</b>", sTblCellC),
cell("Kidneys excrete HCO\u2083\u207b", sTblCellC),
Paragraph("<b>\u0394HCO\u2083\u207b = \u22122 mEq/L per 10 mmHg \u2193 PaCO\u2082</b>", sFormula)],
# Respiratory alkalosis chronic
[Paragraph("<b>Respiratory\nAlkalosis</b>\n<i>(Chronic)</i>", sTblCellC),
Paragraph("<font color='#1A6FAF'><b>\u2191 High</b></font>", sTblCellC),
Paragraph("<b>\u2193 PaCO\u2082</b>", sTblCellC),
cell("Kidneys excrete HCO\u2083\u207b", sTblCellC),
Paragraph("<b>\u0394HCO\u2083\u207b = \u22125 mEq/L per 10 mmHg \u2193 PaCO\u2082</b>", sFormula)],
]
comp_col = [W*0.16, W*0.09, W*0.12, W*0.22, W*0.41]
comp_tbl = Table(comp_data, colWidths=comp_col, repeatRows=1)
comp_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_HEADER_BG),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_ACID_BG, C_ACID_BG, C_ALK_BG, C_NEUTRAL_BG, C_NEUTRAL_BG, C_ALK_BG, C_ALK_BG]),
("GRID", (0,0), (-1,-1), 0.5, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ALIGN", (0,1), (-1,-1), "CENTER"),
]))
story.append(comp_tbl)
story.append(sp(7))
# ── TWO COLUMN: CAUSES & ANION GAP ───────────────────────────────────────────
# Left: Causes table | Right: Anion gap box
# --- CAUSES left column ---
causes_title = section_band(" COMMON CAUSES", full_width=False)
causes_data = [
[hdr_cell("Disorder"), hdr_cell("Common Causes")],
[Paragraph("<font color='#C0392B'><b>Metabolic\nAcidosis</b></font>", sTblCellC),
cell("High-AG: DKA, lactic acidosis, uremia, salicylates,\n"
"methanol, ethylene glycol, starvation ketosis\n"
"Normal-AG: diarrhea, RTA, saline infusion,\nuretero-sigmoidostomy")],
[Paragraph("<font color='#1A6FAF'><b>Metabolic\nAlkalosis</b></font>", sTblCellC),
cell("Vomiting / NG suction, loop/thiazide diuretics,\n"
"mineralocorticoid excess, post-hypercapnia,\nexcessive antacids / NaHCO\u2083")],
[Paragraph("<font color='#C0392B'><b>Respiratory\nAcidosis</b></font>", sTblCellC),
cell("COPD, asthma, opioid / sedative overdose,\nneuromuscular disease, obesity hypoventilation,\npneumothorax, severe pneumonia")],
[Paragraph("<font color='#1A6FAF'><b>Respiratory\nAlkalosis</b></font>", sTblCellC),
cell("Anxiety / pain, fever, sepsis (early), hypoxia,\npregnancy, hepatic failure, iatrogenic\n(excess mechanical ventilation)")],
]
LW = W * 0.52 - 3
cause_col = [LW*0.30, LW*0.70]
causes_tbl = Table(causes_data, colWidths=cause_col, repeatRows=1)
causes_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_HEADER_BG),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_ACID_BG, C_ALK_BG, C_ACID_BG, C_ALK_BG]),
("GRID", (0,0), (-1,-1), 0.5, C_BORDER),
("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"),
]))
# --- Anion gap right column ---
RW = W * 0.48 - 3
def ag_box():
items = []
items.append(section_band(" ANION GAP (AG)", bg=C_GREEN_HDR, full_width=False))
items.append(sp(3))
formula_tbl = Table([[Paragraph(
"AG = Na\u207a \u2212 (Cl\u207b + HCO\u2083\u207b)<br/>"
"Normal: <b>8 \u2013 12 mEq/L</b>",
sFormula)]],
colWidths=[RW])
formula_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_GREEN_BG),
("BOX", (0,0), (-1,-1), 1, C_GREEN_HDR),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
]))
items.append(formula_tbl)
items.append(sp(3))
# High AG causes mnemonic
mudpile = Table([[Paragraph(
"<b>A MUDPILE CAT</b> (High AG causes):<br/>"
"<b>A</b>spirin <b>M</b>ethanol <b>U</b>remia <b>D</b>KA<br/>"
"<b>P</b>araldehyde <b>I</b>soniazid/Iron <b>L</b>actic acidosis<br/>"
"<b>E</b>thylene glycol | <b>C</b>O/Cyanide <b>A</b>lcoholic KA <b>T</b>oluene",
sTblCell)]],
colWidths=[RW])
mudpile.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_YELLOW_BG),
("BOX", (0,0), (-1,-1), 0.8, C_YELLOW_HDR),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
]))
items.append(mudpile)
items.append(sp(3))
# Albumin correction note
alb_tbl = Table([[Paragraph(
"<b>Albumin correction:</b> AG falls ~2.5 mEq/L<br/>"
"for each 1 g/dL \u2193 in albumin below normal.<br/>"
"Corrected AG = AG + 2.5 \u00d7 (4 \u2212 albumin g/dL)",
sTblCell)]],
colWidths=[RW])
alb_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_ROW_ALT),
("BOX", (0,0), (-1,-1), 0.6, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
]))
items.append(alb_tbl)
items.append(sp(3))
# Osmolal gap
osm_tbl = Table([[Paragraph(
"<b>Osmolal Gap:</b> Osm<sub>meas</sub> \u2212 Osm<sub>calc</sub> (Normal \u22645\u201310)<br/>"
"Osm<sub>calc</sub> = 2\u00d7Na + BUN/2.8 + Glucose/18<br/>"
"Elevated gap \u2192 methanol, ethanol, ethylene glycol,\nisopropyl alcohol",
sTblCell)]],
colWidths=[RW])
osm_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), HexColor("#F0E8F8")),
("BOX", (0,0), (-1,-1), 0.6, HexColor("#7B4FA0")),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
]))
items.append(osm_tbl)
return items
# Combine left + right
from reportlab.platypus import BalancedColumns
left_items = [causes_title, sp(3), causes_tbl]
right_items = ag_box()
two_col = Table(
[[left_items, right_items]],
colWidths=[LW + 3, RW + 3]
)
two_col.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 3),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING",(0,0), (-1,-1), 0),
]))
story.append(two_col)
story.append(sp(7))
# ── MIXED DISORDERS ───────────────────────────────────────────────────────────
story.append(section_band(" MIXED ACID-BASE DISORDERS"))
story.append(sp(3))
mixed_data = [
[hdr_cell("Mixed Disorder"), hdr_cell("pH Pattern"), hdr_cell("Key Clue"), hdr_cell("Classic Causes")],
[cell("Met. Acidosis + Resp. Alkalosis", sTblCellB),
cell("pH \u2193 or near-normal; PaCO\u2082 \u2193\u2193; HCO\u2083\u207b \u2193\u2193"),
cell("PaCO\u2082 lower than expected for HCO\u2083\u207b"),
cell("Salicylate toxicity, sepsis, hepatic failure")],
[cell("Met. Alkalosis + Resp. Acidosis", sTblCellB),
cell("pH \u2191 or near-normal; PaCO\u2082 \u2191\u2191; HCO\u2083\u207b \u2191\u2191"),
cell("PaCO\u2082 higher than expected for HCO\u2083\u207b"),
cell("COPD + diuretics / vomiting")],
[cell("Met. Acidosis + Met. Alkalosis", sTblCellB),
cell("pH variable; AG \u2191 but HCO\u2083\u207b higher than expected"),
cell("Delta-Delta ratio > 2"),
cell("Renal failure + vomiting; cirrhosis + diarrhea")],
[cell("Resp. Acidosis + Resp. Alkalosis", sTblCellB),
cell("Not possible (one primary respiratory)"), cell("\u2014"),
cell("CNS lesions causing abnormal breathing patterns")],
[cell("Double Acidosis\n(Met. + Resp. Acidosis)", sTblCellB),
cell("pH \u2193\u2193 severe; PaCO\u2082 \u2191; HCO\u2083\u207b \u2193"),
cell("Point outside compensation band on\nacid-base map"),
cell("Cardiac arrest, severe multi-organ failure")],
]
mixed_col = [W*0.25, W*0.22, W*0.27, W*0.26]
mixed_tbl = Table(mixed_data, colWidths=mixed_col, repeatRows=1)
mixed_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_HEADER_BG),
("ROWBACKGROUNDS",(0,1), (-1,-1), [colors.white, C_ROW_ALT]),
("GRID", (0,0), (-1,-1), 0.5, C_BORDER),
("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"),
]))
story.append(mixed_tbl)
story.append(sp(7))
# ── FLUID-ACID-BASE CONNECTIONS ───────────────────────────────────────────────
story.append(section_band(" FLUID IMBALANCE \u2192 ACID-BASE EFFECTS"))
story.append(sp(3))
fluid_data = [
[hdr_cell("Fluid Disturbance"), hdr_cell("Acid-Base Effect"), hdr_cell("Mechanism")],
[cell("Volume depletion"), cell("Metabolic alkalosis (contraction)"),
cell("HCO\u2083\u207b concentrates as volume \u2193; aldosterone \u2191 H\u207a secretion")],
[cell("Normal saline (0.9% NaCl) infusion"), cell("Non-gap metabolic acidosis"),
cell("Excess Cl\u207b displaces and dilutes HCO\u2083\u207b")],
[cell("Diarrhea"), cell("Non-gap metabolic acidosis"),
cell("Direct HCO\u2083\u207b loss in stool")],
[cell("Vomiting / NG suctioning"), cell("Metabolic alkalosis"),
cell("HCl loss from stomach lumen")],
[cell("Renal failure / oliguria"), cell("High-AG metabolic acidosis"),
cell("Retention of sulfates, phosphates, organic acids")],
[cell("Hypotonic fluid / mannitol"), cell("Dilutional (non-gap) metabolic acidosis"),
cell("Dilution of serum HCO\u2083\u207b")],
]
fluid_col = [W*0.28, W*0.28, W*0.44]
fluid_tbl = Table(fluid_data, colWidths=fluid_col, repeatRows=1)
fluid_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_HEADER_BG),
("ROWBACKGROUNDS",(0,1), (-1,-1), [colors.white, C_ROW_ALT]),
("GRID", (0,0), (-1,-1), 0.5, C_BORDER),
("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"),
]))
story.append(fluid_tbl)
story.append(sp(7))
# ── CLINICAL PEARLS ───────────────────────────────────────────────────────────
story.append(section_band(" CLINICAL PEARLS"))
story.append(sp(3))
pearls = [
("Compensation is never complete",
"pH always remains on the side of the primary disorder. If pH is fully normal, suspect a mixed disorder."),
("Metabolic alkalosis PaCO\u2082 ceiling",
"Respiratory compensation for metabolic alkalosis rarely raises PaCO\u2082 above 55\u201360 mmHg "
"because the resulting hypoxia re-stimulates ventilation."),
("Albumin and the anion gap",
"Hypoalbuminemia lowers the expected AG by ~2.5 mEq/L per 1 g/dL drop. Always correct before "
"concluding the AG is normal."),
("COPD baseline",
"Always check the patient\u2019s baseline PaCO\u2082 before labelling as pure acute respiratory acidosis. "
"A high total CO\u2082 on the chemistry panel predicts chronic CO\u2082 retention."),
("Renal compensation timing",
"Renal compensation is slow (3\u20135 days). In the acute phase of respiratory disorders, "
"HCO\u2083\u207b changes are minimal \u2014 a large HCO\u2083\u207b shift implies chronic or mixed disorder."),
("Saline-responsive vs. saline-resistant metabolic alkalosis",
"Urine Cl\u207b < 20 mEq/L (saline-responsive): volume depletion, vomiting, NG suction.\n"
"Urine Cl\u207b > 20 mEq/L (saline-resistant): hyperaldosteronism, severe K\u207a depletion, Bartter/Gitelman."),
("Winter\u2019s formula check",
"For metabolic acidosis, if actual PaCO\u2082 < expected \u2192 concurrent respiratory alkalosis. "
"If actual PaCO\u2082 > expected \u2192 concurrent respiratory acidosis."),
]
pearl_rows = [[cell("\u2022 " + t, sTblCellB), cell(d)] for t, d in pearls]
pearl_tbl = Table(pearl_rows, colWidths=[W*0.28, W*0.72])
pearl_tbl.setStyle(TableStyle([
("ROWBACKGROUNDS", (0,0), (-1,-1), [HexColor("#F0F5FB"), colors.white]),
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(pearl_tbl)
story.append(sp(8))
# ── FOOTER ────────────────────────────────────────────────────────────────────
story.append(HRFlowable(width=W, thickness=0.5, color=C_BORDER))
story.append(sp(2))
story.append(Paragraph(
"Sources: Roberts & Hedges' Clinical Procedures in Emergency Medicine | "
"NKF Primer on Kidney Diseases, 8e | Ganong's Review of Medical Physiology, 26e | "
"Miller's Anesthesia, 10e | Harrison's Principles of Internal Medicine, 22e (2025) "
"\u00a9 Orris Medical Quick Reference",
sFooter
))
# ── BUILD ─────────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF written to: {OUTPUT}")
cd /home/daytona/workspace/abg-reference && python generate_abg_pdf.py 2>&1
ls -lh /home/daytona/workspace/abg-reference/ABG_Quick_Reference.pdf
ABG Quick Reference
PDF Document · PDF
I want podcast like of this pdf
python3 -c "import pyttsx3; print('pyttsx3 ok')" 2>&1; python3 -c "from gtts import gTTS; print('gtts ok')" 2>&1; which espeak 2>&1; which festival 2>&1; python3 -c "import edge_tts; print('edge_tts ok')" 2>&1
pip install --user gtts 2>&1 | tail -3; pip install --user edge-tts 2>&1 | tail -3
python3 -c "from gtts import gTTS; print('gtts ok')" 2>&1; python3 -c "import edge_tts; print('edge_tts ok')" 2>&1
~/abg-reference/podcast_script.py
import asyncio
import os
import json
import edge_tts
from pydub import AudioSegment
OUTPUT_DIR = "/home/daytona/workspace/abg-reference/segments"
FINAL_MP3 = "/home/daytona/workspace/abg-reference/ABG_Podcast.mp3"
os.makedirs(OUTPUT_DIR, exist_ok=True)
# Voices: Alex = male (en-US-GuyNeural), Sam = female (en-US-JennyNeural)
VOICE_ALEX = "en-US-GuyNeural"
VOICE_SAM = "en-US-JennyNeural"
# ── SCRIPT ────────────────────────────────────────────────────────────────────
# Format: list of (speaker, text) speaker = "ALEX" or "SAM"
SCRIPT = [
# ── INTRO ──
("ALEX", "Welcome to MedReview, the podcast that makes clinical concepts click. I'm Alex."),
("SAM", "And I'm Sam. Today we are diving into arterial blood gas interpretation - ABGs - which is honestly one of those topics that trips up a lot of students and even some residents."),
("ALEX", "Absolutely. And the good news is, once you have a solid framework, it becomes almost mechanical. So Sam, let's start at the very beginning - why do we even care about blood pH?"),
("SAM", "Great question. The human body is incredibly sensitive to pH changes. Normal blood pH sits between 7.35 and 7.45. That's a razor-thin range. Even a shift of 0.1 or 0.2 units can cause serious problems - enzyme dysfunction, altered drug binding, cardiac arrhythmias."),
("ALEX", "And the body has three main tools to keep pH in that range, right?"),
("SAM", "Exactly. First, chemical buffers - mainly the bicarbonate-carbonic acid system, but also proteins and phosphates. These act within seconds. Second, the lungs, which adjust carbon dioxide levels within minutes. And third, the kidneys, which are the most powerful but also the slowest - taking hours to days."),
("ALEX", "So the lungs handle the fast adjustments and the kidneys handle the fine-tuning. Perfect. Let's lock in the normal values before we go further."),
("SAM", "Sure. pH: 7.35 to 7.45. PaCO2 - that's the partial pressure of carbon dioxide in arterial blood - 35 to 45 millimeters of mercury. Bicarbonate, written as HCO3 minus, is 22 to 26 milliequivalents per liter. Base excess: negative 2 to positive 2. PaO2 should be 80 to 100, and oxygen saturation above 95 percent."),
("ALEX", "Memorize those. They are your anchor points for everything that follows."),
# ── STEP BY STEP ──
("SAM", "Alright, let's talk about the five-step approach to reading an ABG. This is the systematic framework you should use every single time."),
("ALEX", "Step one: check the pH. If it's below 7.35, you have acidosis. Above 7.45, alkalosis. Simple enough."),
("SAM", "Step two: identify the primary process. Look at PaCO2 and bicarbonate and ask - which one is moving in the direction that explains the pH change? If the pH is low and PaCO2 is high, that's respiratory acidosis. If the pH is low and bicarbonate is low, that's metabolic acidosis."),
("ALEX", "And the same logic for alkalosis - pH high with low PaCO2 is respiratory alkalosis, and pH high with high bicarbonate is metabolic alkalosis."),
("SAM", "Step three: check the compensation. This is where it gets interesting. The body always tries to compensate for a primary disorder. And the beauty is, we have formulas that tell us exactly how much compensation to expect."),
("ALEX", "And if the actual compensation doesn't match the expected compensation, that's a red flag for a mixed disorder."),
("SAM", "Exactly. Step four: if you've identified a metabolic acidosis, always calculate the anion gap. We'll talk about that in depth in a moment. And step five: if you find a high anion gap acidosis, calculate the delta-delta ratio to check if there's a co-existing metabolic disorder hiding underneath."),
("ALEX", "Five steps. pH, primary process, compensation check, anion gap, delta-delta. Let's make it memorable: Patients Pee Calmly After Diagnosis."),
("SAM", "Ha, I love that. pH, Primary, Compensation, Anion gap, Delta-delta. Perfect."),
# ── FOUR DISORDERS ──
("ALEX", "Okay, let's go through each of the four primary acid-base disorders. Starting with metabolic acidosis."),
("SAM", "Metabolic acidosis: low pH, low bicarbonate. The lungs compensate by hyperventilating - blowing off CO2 to raise the pH back up. You'll sometimes see patients breathing very deeply and rapidly. That pattern is called Kussmaul breathing."),
("ALEX", "The compensation formula here is Winter's formula: expected PaCO2 equals 1.5 times the bicarbonate, plus 8, plus or minus 2. If the actual PaCO2 is lower than expected, there's an additional respiratory alkalosis. If it's higher, there's an added respiratory acidosis."),
("SAM", "Now, the causes of metabolic acidosis split into two big groups based on the anion gap - which we will discuss fully in a moment. High anion gap causes include things like diabetic ketoacidosis, lactic acidosis, uremia from kidney failure, and toxic ingestions like methanol or ethylene glycol. Normal anion gap causes include diarrhea, renal tubular acidosis, and excessive saline administration."),
("ALEX", "Next up, metabolic alkalosis. High pH, high bicarbonate. The lungs compensate by hypoventilating - retaining CO2."),
("SAM", "The compensation formula: the rise in PaCO2 equals 0.6 times the rise in bicarbonate. But here's the catch - this compensation is self-limiting. The body won't let PaCO2 climb above about 55 to 60 millimeters of mercury, because the resulting low oxygen will kick the respiratory drive back in."),
("ALEX", "Common causes: vomiting or nasogastric suctioning - you're losing hydrochloric acid. Diuretics - losing chloride and potassium. Hyperaldosteronism. Post-hypercapnia states where the kidney has held onto a lot of bicarbonate."),
("SAM", "A clinically useful distinction is whether the metabolic alkalosis is saline-responsive or not. Check the urine chloride. If it's below 20, the patient is volume-depleted and will respond to normal saline - think vomiting, NG suction, diuretics. If urine chloride is above 20, they're saline-resistant - think hyperaldosteronism or severe potassium depletion."),
("ALEX", "Now let's talk about respiratory acidosis. High PaCO2, low pH. This is hypoventilation - the lungs aren't blowing off enough CO2."),
("SAM", "The kidney compensates by retaining bicarbonate, but this takes time. In acute respiratory acidosis - within the first few hours - the kidneys only raise bicarbonate by 1 milliequivalent per liter for every 10 millimeter of mercury rise in PaCO2. In chronic respiratory acidosis, after 3 to 5 days, that rises to 3 to 4 milliequivalents per liter per 10 millimeters."),
("ALEX", "That distinction between acute and chronic is huge clinically. A COPD patient with a PaCO2 of 70 and a bicarbonate of 33 - that's chronic compensation. That's their normal. Don't aggressively normalize their CO2 or you'll cause severe metabolic alkalosis."),
("SAM", "Causes of respiratory acidosis: COPD, asthma exacerbation, opioid or sedative overdose, neuromuscular diseases like myasthenia gravis or Guillain-Barre, obesity hypoventilation syndrome, pneumothorax."),
("ALEX", "And finally, respiratory alkalosis. Low PaCO2, high pH. This is hyperventilation. The kidneys compensate by excreting bicarbonate."),
("SAM", "Acute: bicarbonate drops 2 milliequivalents per liter for every 10 millimeter drop in PaCO2. Chronic: 5 milliequivalents per liter per 10 millimeters."),
("ALEX", "Causes include anxiety and pain, fever, early sepsis - actually, respiratory alkalosis is the most common acid-base disorder seen in the ICU and is often the first sign of sepsis. Also hypoxia, pregnancy due to progesterone, liver failure, and excessive mechanical ventilation."),
# ── ANION GAP ──
("SAM", "Let's spend a bit of time on the anion gap because it is such a powerful diagnostic tool."),
("ALEX", "The concept is elegant. In blood, total positive charges must equal total negative charges. We measure the three biggest players: sodium on the positive side, chloride and bicarbonate on the negative side. The anion gap is the difference."),
("SAM", "Anion gap equals sodium, minus the sum of chloride plus bicarbonate. Normal range is 8 to 12 milliequivalents per liter. This gap exists because there are always some unmeasured anions present - mainly albumin, but also phosphate, sulfate, and organic acids."),
("ALEX", "If the anion gap is elevated, it means an unmeasured anion has accumulated in the blood. That anion has replaced bicarbonate, lowering it, without chloride rising to compensate. Hence: high anion gap metabolic acidosis."),
("SAM", "The classic mnemonic for high anion gap causes is A MUDPILE CAT. Let me spell it out: A for Aspirin and salicylates. M for Methanol. U for Uremia. D for Diabetic ketoacidosis. P for Paraldehyde. I for Isoniazid and Iron. L for Lactic acidosis - by far the most common in the hospital. E for Ethylene glycol. Then C for Carbon monoxide and Cyanide. A for Alcoholic ketoacidosis. T for Toluene."),
("ALEX", "Perfect. Now there's an important caveat. Albumin is the main contributor to the normal anion gap. So in patients with low albumin - which is very common in sick patients - the expected anion gap is lower. For every 1 gram per deciliter drop in albumin below normal, the anion gap falls by about 2.5 milliequivalents per liter."),
("SAM", "So if your patient has an albumin of 2 instead of 4, their corrected normal anion gap should be around 7, not 10. A measured anion gap of 10 in that patient is actually elevated. You would miss the high-gap acidosis if you don't correct for albumin."),
("ALEX", "Great point. Now, the osmolal gap. This is another tool used alongside the anion gap in suspected toxic ingestions. You calculate the expected osmolality using: 2 times sodium, plus BUN divided by 2.8, plus glucose divided by 18. Then you compare it to the measured osmolality. Normal gap is less than 10."),
("SAM", "An elevated osmolal gap - say above 20 - tells you there's an unmeasured osmotically active substance in the blood. Think methanol, ethylene glycol, isopropyl alcohol, or ethanol. If you have both a high anion gap AND a high osmolal gap, methanol or ethylene glycol poisoning shoots to the top of your differential."),
# ── DELTA DELTA ──
("ALEX", "Now let's tackle the delta-delta ratio. This sounds intimidating but the concept is actually quite intuitive."),
("SAM", "When you have a high anion gap metabolic acidosis, the rise in the anion gap should be matched by a corresponding fall in bicarbonate. Because the unmeasured anion replaced bicarbonate one-for-one."),
("ALEX", "So the delta-delta is: the rise in anion gap - that's the measured AG minus 12 - divided by the fall in bicarbonate - that's 24 minus the measured bicarbonate."),
("SAM", "If the ratio is between 1 and 2, you have a pure high anion gap acidosis. That's the expected proportional change. If the ratio is below 1, the bicarbonate fell more than expected - meaning there's an additional normal anion gap acidosis on top. If the ratio is above 2, the bicarbonate is higher than expected - meaning there's a co-existing metabolic alkalosis that's been partially masked by the acidosis."),
("ALEX", "That last one is particularly sneaky. A patient with DKA and vomiting might have a pH that looks like straightforward ketoacidosis, but the delta-delta reveals a hidden metabolic alkalosis from the vomiting."),
# ── MIXED DISORDERS ──
("SAM", "Let's briefly cover mixed disorders. These are cases where two or more primary disorders exist simultaneously."),
("ALEX", "The most common combination in clinical practice is metabolic acidosis with respiratory alkalosis. You see this in salicylate poisoning, sepsis, and liver failure. The pH might look near-normal, but both the PaCO2 and bicarbonate are low - lower than would be explained by compensation alone."),
("SAM", "Metabolic alkalosis plus respiratory acidosis is classic in COPD patients on diuretics or with vomiting. The CO2 is chronically high and the bicarbonate is high, pushing the pH toward normal or even slightly alkalotic."),
("ALEX", "And the most dangerous mix - double acidosis. Both respiratory and metabolic acidosis at the same time. Cardiac arrest is the classic example. pH crashes dramatically. You need to fix both the ventilation and the underlying metabolic cause simultaneously."),
("SAM", "The key to identifying mixed disorders is always comparing actual compensation against expected compensation using the formulas. If they don't match, something else is going on."),
# ── FLUIDS ──
("ALEX", "One more topic that ties nicely into this - how do IV fluids and fluid imbalances affect acid-base status?"),
("SAM", "This is so clinically relevant. Normal saline - which is 0.9% sodium chloride - is actually quite acidic to give in large volumes. It causes a hyperchloremic non-gap metabolic acidosis because the excess chloride displaces bicarbonate."),
("ALEX", "Balanced crystalloids like lactated Ringer's or Plasma-Lyte are much closer to physiologic and don't cause this chloride-driven acidosis. There's actually good evidence now that balanced crystalloids lead to better outcomes in critically ill patients compared to normal saline."),
("SAM", "Vomiting and NG suctioning cause metabolic alkalosis by removing hydrochloric acid. Diarrhea causes metabolic acidosis by losing bicarbonate-rich intestinal fluid. Volume depletion from any cause can cause contraction alkalosis - the bicarbonate concentrates as the extracellular fluid volume shrinks."),
("ALEX", "And in acute kidney injury or chronic kidney disease, the failing kidneys can't excrete acid, so sulfates, phosphates, and organic acids accumulate - giving you a high anion gap metabolic acidosis."),
# ── PEARLS ──
("SAM", "Let's wrap up with some high-yield clinical pearls. First: compensation is never complete. The pH always stays on the side of the primary disorder. If someone has a perfectly normal pH, suspect a mixed disorder."),
("ALEX", "Second: always know your COPD patient's baseline. Their normal PaCO2 might be 60 or 70. Treating that as acute hypercapnia and over-ventilating them will cause severe metabolic alkalosis."),
("SAM", "Third: correct the anion gap for albumin in any critically ill patient. Low albumin is very common in the ICU and will mask an elevated anion gap."),
("ALEX", "Fourth: metabolic alkalosis compensation through hypoventilation is self-limited. The body won't tolerate PaCO2 above about 55 to 60 millimeters of mercury because hypoxia will override the drive to retain CO2."),
("SAM", "Fifth: in sepsis, watch for respiratory alkalosis as an early sign. Lactic acidosis comes later. If you see a patient with a slightly low PaCO2 and high-normal pH before any obvious acidosis, be suspicious."),
("ALEX", "And sixth - the one that trips people up on exams and in practice: if you have metabolic acidosis with a high anion gap and the delta-delta ratio is over 2, there's a hidden metabolic alkalosis underneath. Don't miss it."),
# ── OUTRO ──
("SAM", "Alright, let's do a quick recap of everything we covered today."),
("ALEX", "Normal values: pH 7.35 to 7.45, PaCO2 35 to 45, bicarbonate 22 to 26."),
("SAM", "Five steps: check pH, identify primary process, calculate expected compensation, compute anion gap if metabolic acidosis, and run the delta-delta if high anion gap."),
("ALEX", "Four disorders: metabolic acidosis, metabolic alkalosis, respiratory acidosis, respiratory alkalosis - each with distinct compensation formulas for acute versus chronic cases."),
("SAM", "Anion gap: sodium minus chloride plus bicarbonate. Normal 8 to 12. Use A MUDPILE CAT for high-gap causes. Correct for albumin. Add osmolal gap when toxic ingestion is suspected."),
("ALEX", "Mixed disorders: compare actual versus expected compensation. Delta-delta ratio uncovers hidden concurrent metabolic disorders."),
("SAM", "And fluid-acid-base connections: saline causes hyperchloremic acidosis, vomiting causes alkalosis, diarrhea causes acidosis, renal failure causes high-gap acidosis."),
("ALEX", "That's a wrap on ABG interpretation. If you found this helpful, share it with a colleague who's staring at a blood gas result right now and feeling lost."),
("SAM", "Until next time - stay curious, stay clinical. This is Sam."),
("ALEX", "And Alex. Take care everyone."),
]
print(f"Script has {len(SCRIPT)} lines")
for i, (spk, txt) in enumerate(SCRIPT):
print(f" {i:03d} [{spk}] {txt[:60]}...")
cd /home/daytona/workspace/abg-reference && python podcast_script.py 2>&1 | head -20
pip install --user pydub 2>&1 | tail -2; which ffmpeg 2>&1; which avconv 2>&1
~/abg-reference/generate_podcast.py
import asyncio
import os
import subprocess
SEG_DIR = "/home/daytona/workspace/abg-reference/segments"
FINAL_MP3 = "/home/daytona/workspace/abg-reference/ABG_Podcast.mp3"
os.makedirs(SEG_DIR, exist_ok=True)
VOICE_ALEX = "en-US-GuyNeural" # male host
VOICE_SAM = "en-US-JennyNeural" # female host
SCRIPT = [
("ALEX", "Welcome to MedReview, the podcast that makes clinical concepts click. I'm Alex."),
("SAM", "And I'm Sam. Today we are diving into arterial blood gas interpretation, A B G s, which is one of those topics that trips up a lot of students and even some residents."),
("ALEX", "Absolutely. And the good news is, once you have a solid framework, it becomes almost mechanical. So Sam, let's start at the very beginning. Why do we care about blood pH?"),
("SAM", "Great question. The human body is incredibly sensitive to pH changes. Normal blood pH sits between 7.35 and 7.45. That is a razor-thin range. Even a shift of 0.1 or 0.2 units can cause serious problems, enzyme dysfunction, altered drug binding, cardiac arrhythmias."),
("ALEX", "And the body has three main tools to keep pH in that range, right?"),
("SAM", "Exactly. First, chemical buffers, mainly the bicarbonate carbonic acid system, but also proteins and phosphates. These act within seconds. Second, the lungs, which adjust carbon dioxide levels within minutes. And third, the kidneys, which are the most powerful but also the slowest, taking hours to days."),
("ALEX", "So the lungs handle fast adjustments and the kidneys handle the fine tuning. Let's lock in the normal values before we go further."),
("SAM", "Sure. pH: 7.35 to 7.45. PaCO2, the partial pressure of carbon dioxide in arterial blood: 35 to 45 millimeters of mercury. Bicarbonate: 22 to 26 milliequivalents per liter. Base excess: negative 2 to positive 2. PaO2 should be 80 to 100, and oxygen saturation above 95 percent."),
("ALEX", "Memorize those. They are your anchor points for everything that follows."),
("SAM", "Alright, let's talk about the five step approach to reading an A B G. This is the systematic framework you should use every single time."),
("ALEX", "Step one: check the pH. Below 7.35 is acidosis. Above 7.45 is alkalosis. Simple."),
("SAM", "Step two: identify the primary process. Look at PaCO2 and bicarbonate and ask, which one is moving in the direction that explains the pH change? If pH is low and PaCO2 is high, that is respiratory acidosis. If pH is low and bicarbonate is low, that is metabolic acidosis."),
("ALEX", "And the same logic for alkalosis. pH high with low PaCO2 is respiratory alkalosis, and pH high with high bicarbonate is metabolic alkalosis."),
("SAM", "Step three: check the compensation. The body always tries to compensate for a primary disorder, and we have formulas that tell us exactly how much compensation to expect."),
("ALEX", "If the actual compensation does not match the expected compensation, that is a red flag for a mixed disorder."),
("SAM", "Step four: if you have identified metabolic acidosis, always calculate the anion gap. We will talk about that in depth in a moment. And step five: if you find a high anion gap acidosis, calculate the delta-delta ratio to check if there is a co-existing metabolic disorder hiding underneath."),
("ALEX", "Five steps. To make it memorable, try this: Patients Pee Calmly After Diagnosis. pH, Primary process, Compensation, Anion gap, Delta-delta."),
("SAM", "Ha, I love that. Let's go through each of the four primary acid-base disorders. Starting with metabolic acidosis."),
("ALEX", "Metabolic acidosis: low pH, low bicarbonate. The lungs compensate by hyperventilating, blowing off carbon dioxide to raise the pH back up. You will sometimes see patients breathing very deeply and rapidly. That pattern is called Kussmaul breathing."),
("SAM", "The compensation formula here is Winter's formula. Expected PaCO2 equals 1.5 times the bicarbonate, plus 8, plus or minus 2. If the actual PaCO2 is lower than expected, there is an additional respiratory alkalosis. If it is higher, there is an added respiratory acidosis."),
("ALEX", "The causes of metabolic acidosis split into two big groups based on the anion gap. High anion gap causes include diabetic ketoacidosis, lactic acidosis, uremia from kidney failure, and toxic ingestions like methanol or ethylene glycol. Normal anion gap causes include diarrhea, renal tubular acidosis, and excessive saline administration."),
("SAM", "Next: metabolic alkalosis. High pH, high bicarbonate. The lungs compensate by hypoventilating, retaining carbon dioxide. The formula: rise in PaCO2 equals 0.6 times the rise in bicarbonate. But compensation is self-limiting. The body will not let PaCO2 climb above about 55 to 60 millimeters of mercury because the resulting low oxygen kicks the respiratory drive back in."),
("ALEX", "Common causes: vomiting or nasogastric suctioning, you are losing hydrochloric acid. Diuretics, losing chloride and potassium. Hyperaldosteronism. Post-hypercapnia states where the kidney has held onto a lot of bicarbonate."),
("SAM", "A clinically useful distinction is whether the metabolic alkalosis is saline-responsive or not. Check the urine chloride. Below 20 milliequivalents per liter means volume depleted and will respond to normal saline. Think vomiting, NG suction, diuretics. Urine chloride above 20 means saline resistant. Think hyperaldosteronism or severe potassium depletion."),
("ALEX", "Now, respiratory acidosis. High PaCO2, low pH. This is hypoventilation. The lungs are not blowing off enough carbon dioxide. The kidney compensates by retaining bicarbonate, but this takes time."),
("SAM", "In acute respiratory acidosis, within the first few hours, the kidneys only raise bicarbonate by 1 milliequivalent per liter for every 10 millimeter rise in PaCO2. In chronic respiratory acidosis, after 3 to 5 days, that rises to 3 to 4 milliequivalents per liter."),
("ALEX", "That distinction is huge clinically. A C O P D patient with a PaCO2 of 70 and a bicarbonate of 33 - that is their normal. Do not aggressively normalize their CO2 or you will cause severe metabolic alkalosis."),
("SAM", "Causes of respiratory acidosis: COPD, asthma exacerbation, opioid or sedative overdose, neuromuscular diseases like myasthenia gravis or Guillain-Barre, obesity hypoventilation syndrome, and pneumothorax."),
("ALEX", "Finally, respiratory alkalosis. Low PaCO2, high pH. Hyperventilation. The kidneys compensate by excreting bicarbonate. Acute: bicarbonate drops 2 milliequivalents per liter for every 10 millimeter drop in PaCO2. Chronic: 5 milliequivalents per liter per 10 millimeters."),
("SAM", "Causes include anxiety and pain, fever, early sepsis, and respiratory alkalosis is actually the most common acid-base disorder seen in the ICU. Also hypoxia, pregnancy due to progesterone, liver failure, and excessive mechanical ventilation."),
("ALEX", "Let's spend time on the anion gap because it is such a powerful diagnostic tool. The concept is elegant. Total positive charges must equal total negative charges. We measure sodium on the positive side, chloride and bicarbonate on the negative side. The gap is the difference."),
("SAM", "Anion gap equals sodium, minus the sum of chloride plus bicarbonate. Normal range is 8 to 12 milliequivalents per liter. If the anion gap is elevated, an unmeasured anion has accumulated, replacing bicarbonate without chloride rising to compensate. Hence: high anion gap metabolic acidosis."),
("ALEX", "The classic mnemonic for high anion gap causes is A MUDPILE CAT. A for Aspirin and salicylates. M for Methanol. U for Uremia. D for Diabetic ketoacidosis. P for Paraldehyde. I for Isoniazid and Iron. L for Lactic acidosis, by far the most common in the hospital. E for Ethylene glycol. C for Carbon monoxide and Cyanide. A for Alcoholic ketoacidosis. T for Toluene."),
("SAM", "Now here is an important caveat. Albumin is the main contributor to the normal anion gap. In patients with low albumin, which is very common in sick patients, the expected anion gap is lower. For every 1 gram per deciliter drop in albumin below normal, the anion gap falls by about 2.5 milliequivalents per liter. Always correct for albumin in the ICU."),
("ALEX", "The osmolal gap is another tool. Calculate expected osmolality using 2 times sodium, plus BUN divided by 2.8, plus glucose divided by 18. Compare to measured osmolality. Normal gap is less than 10. An elevated osmolal gap tells you there is an unmeasured osmotically active substance in the blood. Think methanol, ethylene glycol, isopropyl alcohol."),
("SAM", "If you have both a high anion gap AND a high osmolal gap, methanol or ethylene glycol poisoning shoots to the top of your differential."),
("ALEX", "Now let's tackle the delta-delta ratio. When you have a high anion gap metabolic acidosis, the rise in the anion gap should match the fall in bicarbonate, because the unmeasured anion replaced bicarbonate one-for-one."),
("SAM", "Delta-delta equals the rise in anion gap, that is measured A G minus 12, divided by the fall in bicarbonate, that is 24 minus the measured bicarbonate. A ratio of 1 to 2 means pure high anion gap acidosis. Below 1 means the bicarbonate fell more than expected, so there is an additional normal anion gap acidosis on top. Above 2 means bicarbonate is higher than expected, so there is a co-existing metabolic alkalosis that has been masked."),
("ALEX", "That last one is particularly sneaky. A patient with D K A and vomiting might have a pH that looks like straightforward ketoacidosis, but the delta-delta reveals a hidden metabolic alkalosis from the vomiting."),
("SAM", "Let's briefly cover mixed disorders. These are cases where two or more primary disorders exist simultaneously. The most common combination is metabolic acidosis with respiratory alkalosis. You see this in salicylate poisoning, sepsis, and liver failure."),
("ALEX", "Metabolic alkalosis plus respiratory acidosis is classic in COPD patients on diuretics or with vomiting. And the most dangerous mix is double acidosis - both respiratory and metabolic at the same time. Cardiac arrest is the classic example. pH crashes dramatically. You need to fix both ventilation and the underlying metabolic cause simultaneously."),
("SAM", "The key to identifying mixed disorders is always comparing actual compensation against expected compensation using the formulas. If they do not match, something else is going on."),
("ALEX", "Let's tie this to fluids. Normal saline given in large volumes causes a hyperchloremic non-gap metabolic acidosis because excess chloride displaces bicarbonate. Balanced crystalloids like lactated Ringer's or Plasma-Lyte are much more physiologic and avoid this problem."),
("SAM", "Vomiting and NG suctioning cause metabolic alkalosis by removing hydrochloric acid. Diarrhea causes metabolic acidosis by losing bicarbonate-rich intestinal fluid. Volume depletion from any cause can cause contraction alkalosis, the bicarbonate concentrates as extracellular fluid volume shrinks. And in kidney failure, sulfates, phosphates, and organic acids accumulate, giving you a high anion gap metabolic acidosis."),
("ALEX", "Let's wrap up with the high-yield clinical pearls. First: compensation is never complete. pH always stays on the side of the primary disorder. If someone has a perfectly normal pH, suspect a mixed disorder."),
("SAM", "Second: always know your C O P D patient's baseline. Their normal PaCO2 might be 60 or 70. Over-ventilating them will cause severe metabolic alkalosis. Third: correct the anion gap for albumin in any critically ill patient. Fourth: metabolic alkalosis compensation is self-limited at a PaCO2 of about 55 to 60 millimeters."),
("ALEX", "Fifth: in sepsis, watch for respiratory alkalosis as an early sign. Lactic acidosis comes later. Sixth: if your delta-delta ratio is over 2 with a high anion gap acidosis, there is a hidden metabolic alkalosis underneath. Do not miss it."),
("SAM", "Alright, quick recap. Normal values: pH 7.35 to 7.45, PaCO2 35 to 45, bicarbonate 22 to 26. Five steps: pH, primary process, expected compensation, anion gap, delta-delta. Four disorders with their compensation formulas. A MUDPILE CAT for high gap causes. Correct anion gap for albumin. And know how fluids affect acid-base balance."),
("ALEX", "That's a wrap on A B G interpretation. If you found this helpful, share it with a colleague who is staring at a blood gas result right now and feeling lost."),
("SAM", "Until next time, stay curious, stay clinical. This is Sam."),
("ALEX", "And Alex. Take care everyone."),
]
async def generate_segment(index, speaker, text, voice):
out_path = os.path.join(SEG_DIR, f"{index:03d}_{speaker}.mp3")
if os.path.exists(out_path):
return out_path
communicate = edge_tts.Communicate(text, voice, rate="+5%", pitch="+0Hz")
await communicate.save(out_path)
return out_path
async def main():
import edge_tts
print(f"Generating {len(SCRIPT)} audio segments...")
tasks = []
for i, (spk, txt) in enumerate(SCRIPT):
voice = VOICE_ALEX if spk == "ALEX" else VOICE_SAM
tasks.append(generate_segment(i, spk, txt, voice))
# Run in batches of 5 to avoid overwhelming the API
segment_paths = []
for batch_start in range(0, len(tasks), 5):
batch = tasks[batch_start:batch_start+5]
results = await asyncio.gather(*batch)
segment_paths.extend(results)
print(f" Done segments {batch_start+1} to {min(batch_start+5, len(tasks))}/{len(tasks)}")
print("All segments generated.")
return segment_paths
if __name__ == "__main__":
import edge_tts
paths = asyncio.run(main())
# Write ffmpeg concat list
concat_file = os.path.join(SEG_DIR, "concat.txt")
with open(concat_file, "w") as f:
for p in paths:
f.write(f"file '{p}'\n")
# Add 400ms silence between segments for natural pacing
silence_path = os.path.join(SEG_DIR, "silence.mp3")
subprocess.run([
"ffmpeg", "-y", "-f", "lavfi", "-i", "anullsrc=r=24000:cl=mono",
"-t", "0.4", "-q:a", "9", "-acodec", "libmp3lame", silence_path
], check=True, capture_output=True)
# Interleave silence between segments
interleaved_file = os.path.join(SEG_DIR, "concat_with_silence.txt")
with open(interleaved_file, "w") as f:
for p in paths:
f.write(f"file '{p}'\n")
f.write(f"file '{silence_path}'\n")
print("Merging all segments into final MP3...")
result = subprocess.run([
"ffmpeg", "-y", "-f", "concat", "-safe", "0",
"-i", interleaved_file,
"-c:a", "libmp3lame", "-q:a", "4", "-ar", "44100",
FINAL_MP3
], capture_output=True, text=True)
if result.returncode == 0:
size = os.path.getsize(FINAL_MP3) / (1024*1024)
print(f"SUCCESS! Final podcast saved to: {FINAL_MP3}")
print(f"File size: {size:.1f} MB")
else:
print("FFMPEG ERROR:", result.stderr[-500:])
cd /home/daytona/workspace/abg-reference && python generate_podcast.py 2>&1
ls -lh /home/daytona/workspace/abg-reference/ABG_Podcast.mp3 2>&1; ffprobe -v quiet -show_entries format=duration,size,bit_rate -of default=noprint_wrappers=1 /home/daytona/workspace/abg-reference/ABG_Podcast.mp3 2>&1
ls /home/daytona/workspace/abg-reference/ && ls /home/daytona/workspace/abg-reference/segments/ | head -10
# Build concat list with silence interleaved SEG_DIR="/home/daytona/workspace/abg-reference/segments" OUT="/home/daytona/workspace/abg-reference/segments/final_podcast.mp3" # Generate a 400ms silence file ffmpeg -y -f lavfi -i anullsrc=r=24000:cl=mono -t 0.4 -q:a 9 -acodec libmp3lame "$SEG_DIR/silence.mp3" 2>/dev/null # Build concat file CONCAT="$SEG_DIR/concat_silence.txt" > "$CONCAT" for f in $(ls "$SEG_DIR"/*.mp3 | grep -v silence | grep -v final | sort); do echo "file '$f'" >> "$CONCAT" echo "file '$SEG_DIR/silence.mp3'" >> "$CONCAT" done echo "Concat list has $(wc -l < $CONCAT) lines" # Merge ffmpeg -y -f concat -safe 0 -i "$CONCAT" -c:a libmp3lame -q:a 3 -ar 44100 "$OUT" 2>&1 | tail -5 ls -lh "$OUT"