Make osce case on dka ,blunt abdominal trauma , secondary sponatous pneumothorax in copd

Asking for Preferences
Reading File
Reading File
Finding Sources
Finding Sources
Finding Sources
Reading File
Reading File
Reading File
Reading File
Reading File
Reading File
Reading File
Reading File
Reading File
Running Command

mkdir -p /home/daytona/workspace/osce-cases && ls /home/daytona/workspace/

Writing File

~/osce-cases/generate_osce.py

from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    HRFlowable, PageBreak, KeepTogether
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import Frame, PageTemplate
from reportlab.lib.colors import HexColor

OUTPUT_PATH = "/home/daytona/workspace/osce-cases/OSCE_Cases.pdf"

# ── Colour palette ──────────────────────────────────────────────────────────
NAVY      = HexColor("#1A3C5E")
TEAL      = HexColor("#0E7C86")
GOLD      = HexColor("#C9A84C")
LIGHTBLUE = HexColor("#D6EAF8")
LIGHTGREY = HexColor("#F2F3F4")
WHITE     = colors.white
BLACK     = colors.black
RED       = HexColor("#C0392B")
GREEN     = HexColor("#1E8449")
ORANGE    = HexColor("#E67E22")

def build_pdf():
    doc = SimpleDocTemplate(
        OUTPUT_PATH,
        pagesize=A4,
        rightMargin=2*cm, leftMargin=2*cm,
        topMargin=2.5*cm, bottomMargin=2*cm,
        title="OSCE Cases – Emergency Medicine",
        author="Orris Medical Education"
    )

    styles = getSampleStyleSheet()

    # Custom styles
    cover_title = ParagraphStyle("cover_title", fontSize=28, textColor=WHITE,
                                  alignment=TA_CENTER, fontName="Helvetica-Bold",
                                  spaceAfter=6)
    cover_sub   = ParagraphStyle("cover_sub", fontSize=14, textColor=GOLD,
                                  alignment=TA_CENTER, fontName="Helvetica",
                                  spaceAfter=4)
    cover_info  = ParagraphStyle("cover_info", fontSize=10, textColor=WHITE,
                                  alignment=TA_CENTER, fontName="Helvetica",
                                  spaceAfter=3)

    h1 = ParagraphStyle("h1", fontSize=16, textColor=WHITE,
                         fontName="Helvetica-Bold", alignment=TA_LEFT,
                         spaceAfter=4, spaceBefore=2,
                         leftIndent=8, borderPad=4)

    h2 = ParagraphStyle("h2", fontSize=12, textColor=NAVY,
                         fontName="Helvetica-Bold", alignment=TA_LEFT,
                         spaceAfter=4, spaceBefore=8,
                         borderColor=TEAL, borderWidth=0,
                         borderPad=2)

    h3 = ParagraphStyle("h3", fontSize=10, textColor=TEAL,
                         fontName="Helvetica-Bold", alignment=TA_LEFT,
                         spaceAfter=3, spaceBefore=5)

    body = ParagraphStyle("body", fontSize=9.5, textColor=BLACK,
                           fontName="Helvetica", alignment=TA_JUSTIFY,
                           spaceAfter=3, leading=14)

    bullet = ParagraphStyle("bullet", fontSize=9.5, textColor=BLACK,
                              fontName="Helvetica", alignment=TA_LEFT,
                              spaceAfter=2, leading=13, leftIndent=14,
                              bulletIndent=4)

    mark_q  = ParagraphStyle("mark_q", fontSize=9.5, textColor=NAVY,
                               fontName="Helvetica-Bold", spaceAfter=2,
                               spaceBefore=4, leading=13)

    mark_a  = ParagraphStyle("mark_a", fontSize=9.5, textColor=BLACK,
                               fontName="Helvetica", spaceAfter=2, leading=13,
                               leftIndent=12)

    story = []

    # ══════════════════════════════════════════════════════════════════════════
    # COVER PAGE
    # ══════════════════════════════════════════════════════════════════════════
    cover_table = Table(
        [[Paragraph("OSCE CLINICAL CASES", cover_title)],
         [Paragraph("Emergency Medicine Series", cover_sub)],
         [Paragraph("For Medical Students – Clinical Years", cover_info)],
         [Paragraph(" ", cover_info)],
         [Paragraph("Case 1  •  Diabetic Ketoacidosis (DKA)", cover_info)],
         [Paragraph("Case 2  •  Blunt Abdominal Trauma", cover_info)],
         [Paragraph("Case 3  •  Secondary Spontaneous Pneumothorax in COPD", cover_info)],
         [Paragraph(" ", cover_info)],
         [Paragraph("Full Case  |  Marking Scheme  |  Model Answers", cover_info)],
         [Paragraph("June 2026", cover_info)],
        ],
        colWidths=[17*cm]
    )
    cover_table.setStyle(TableStyle([
        ("BACKGROUND", (0,0), (-1,-1), NAVY),
        ("ROWBACKGROUNDS", (0,0), (-1,-1), [NAVY]),
        ("TOPPADDING", (0,0), (-1,-1), 10),
        ("BOTTOMPADDING", (0,0), (-1,-1), 10),
        ("LEFTPADDING", (0,0), (-1,-1), 20),
        ("RIGHTPADDING", (0,0), (-1,-1), 20),
        ("BOX", (0,0),(-1,-1), 3, GOLD),
    ]))
    story.append(Spacer(1, 3*cm))
    story.append(cover_table)
    story.append(PageBreak())

    # ══════════════════════════════════════════════════════════════════════════
    # HELPER: section header band
    # ══════════════════════════════════════════════════════════════════════════
    def section_header(text, color=TEAL):
        t = Table([[Paragraph(text, h1)]], colWidths=[17*cm])
        t.setStyle(TableStyle([
            ("BACKGROUND", (0,0),(-1,-1), color),
            ("TOPPADDING", (0,0),(-1,-1), 7),
            ("BOTTOMPADDING", (0,0),(-1,-1), 7),
            ("LEFTPADDING", (0,0),(-1,-1), 10),
            ("BOX", (0,0),(-1,-1), 1, color),
        ]))
        return t

    def info_box(rows, header_color=LIGHTBLUE):
        """Two-column key-value table."""
        data = []
        for k, v in rows:
            data.append([
                Paragraph(f"<b>{k}</b>", ParagraphStyle("kv_k", fontSize=9,
                    fontName="Helvetica-Bold", textColor=NAVY)),
                Paragraph(v, ParagraphStyle("kv_v", fontSize=9,
                    fontName="Helvetica", textColor=BLACK, leading=13))
            ])
        t = Table(data, colWidths=[4.5*cm, 12*cm])
        t.setStyle(TableStyle([
            ("BACKGROUND", (0,0),(-1,-1), header_color),
            ("GRID", (0,0),(-1,-1), 0.5, colors.grey),
            ("TOPPADDING", (0,0),(-1,-1), 4),
            ("BOTTOMPADDING", (0,0),(-1,-1), 4),
            ("LEFTPADDING", (0,0),(-1,-1), 6),
            ("RIGHTPADDING", (0,0),(-1,-1), 6),
            ("VALIGN", (0,0),(-1,-1), "TOP"),
        ]))
        return t

    def marking_table(items):
        """items = list of (question, marks, model_answer)"""
        header = [
            Paragraph("<b>#</b>", ParagraphStyle("th", fontSize=9, fontName="Helvetica-Bold",
                textColor=WHITE, alignment=TA_CENTER)),
            Paragraph("<b>Question / Task</b>", ParagraphStyle("th", fontSize=9, fontName="Helvetica-Bold",
                textColor=WHITE)),
            Paragraph("<b>Marks</b>", ParagraphStyle("th", fontSize=9, fontName="Helvetica-Bold",
                textColor=WHITE, alignment=TA_CENTER)),
            Paragraph("<b>Model Answer</b>", ParagraphStyle("th", fontSize=9, fontName="Helvetica-Bold",
                textColor=WHITE)),
        ]
        data = [header]
        total = 0
        for i, (q, m, ans) in enumerate(items, 1):
            total += m
            data.append([
                Paragraph(str(i), ParagraphStyle("td_n", fontSize=9, fontName="Helvetica",
                    alignment=TA_CENTER)),
                Paragraph(q, ParagraphStyle("td_q", fontSize=9, fontName="Helvetica-Bold",
                    textColor=NAVY, leading=13)),
                Paragraph(str(m), ParagraphStyle("td_m", fontSize=9, fontName="Helvetica",
                    alignment=TA_CENTER)),
                Paragraph(ans, ParagraphStyle("td_a", fontSize=9, fontName="Helvetica",
                    leading=13)),
            ])
        data.append([
            Paragraph("", body), Paragraph("<b>TOTAL</b>", ParagraphStyle("tot", fontSize=10,
                fontName="Helvetica-Bold", textColor=NAVY, alignment=TA_LEFT)),
            Paragraph(f"<b>{total}</b>", ParagraphStyle("tot2", fontSize=10, fontName="Helvetica-Bold",
                textColor=NAVY, alignment=TA_CENTER)),
            Paragraph("", body)
        ])
        t = Table(data, colWidths=[1*cm, 5*cm, 1.5*cm, 9.5*cm])
        t.setStyle(TableStyle([
            ("BACKGROUND", (0,0),(-1,0), NAVY),
            ("BACKGROUND", (0,-1),(-1,-1), LIGHTBLUE),
            ("ROWBACKGROUNDS", (0,1),(-1,-2), [WHITE, LIGHTGREY]),
            ("GRID", (0,0),(-1,-1), 0.5, colors.grey),
            ("TOPPADDING", (0,0),(-1,-1), 5),
            ("BOTTOMPADDING", (0,0),(-1,-1), 5),
            ("LEFTPADDING", (0,0),(-1,-1), 5),
            ("RIGHTPADDING", (0,0),(-1,-1), 5),
            ("VALIGN", (0,0),(-1,-1), "TOP"),
        ]))
        return t

    # ══════════════════════════════════════════════════════════════════════════
    # ══════  CASE 1: DKA  ════════════════════════════════════════════════════
    # ══════════════════════════════════════════════════════════════════════════
    story.append(section_header("OSCE STATION 1  —  Diabetic Ketoacidosis (DKA)", NAVY))
    story.append(Spacer(1, 4*mm))
    story.append(info_box([
        ("Station type",   "History, Examination, Investigations & Management"),
        ("Time allowed",   "10 minutes"),
        ("Total marks",    "25"),
        ("Level",          "Clinical year medical students"),
        ("Competencies",   "Hyperglycaemic emergencies, acid-base physiology, resuscitation"),
    ]))
    story.append(Spacer(1, 5*mm))

    story.append(section_header("CANDIDATE INSTRUCTIONS", TEAL))
    story.append(Spacer(1, 3*mm))
    story.append(Paragraph(
        "You are the doctor on call in the Emergency Department. A 19-year-old female, "
        "<b>Sara</b>, is brought in by her mother. She has a known history of <b>Type 1 Diabetes Mellitus</b>. "
        "Over the past <b>2 days</b> she has had vomiting, abdominal pain, and progressive lethargy. "
        "Her mother says she has been urinating frequently and has not been able to keep food down. "
        "She missed her insulin doses yesterday because 'she wasn't eating.'",
        body))
    story.append(Spacer(1, 3*mm))

    story.append(Paragraph("Vital Signs on Arrival:", h3))
    story.append(info_box([
        ("HR",          "128 bpm (regular)"),
        ("BP",          "94/62 mmHg"),
        ("RR",          "30 breaths/min (deep, laboured)"),
        ("SpO₂",        "97% on room air"),
        ("Temperature", "37.4 °C"),
        ("GCS",         "13/15 (E3 V4 M6)"),
        ("BGL",         "28.4 mmol/L (511 mg/dL)"),
    ], LIGHTGREY))
    story.append(Spacer(1, 4*mm))

    story.append(Paragraph("Tasks:", h3))
    for t_item in [
        "1. Take a focused history from the patient's mother (2 min)",
        "2. Interpret the arterial blood gas (ABG) result provided",
        "3. State your top diagnosis and the diagnostic criteria",
        "4. List the immediate investigations you would request",
        "5. Outline your initial management plan",
        "6. Answer the examiner's questions on complications and monitoring",
    ]:
        story.append(Paragraph(f"• {t_item}", bullet))
    story.append(Spacer(1, 4*mm))

    story.append(Paragraph("ABG Result (Room Air):", h3))
    story.append(info_box([
        ("pH",    "7.18   (normal 7.35–7.45)"),
        ("PaCO₂", "18 mmHg  (normal 35–45) — compensatory hyperventilation"),
        ("PaO₂",  "98 mmHg"),
        ("HCO₃⁻", "7 mmol/L  (normal 22–26)"),
        ("BE",    "–18 mmol/L"),
        ("Lactate","1.8 mmol/L"),
        ("Na⁺",   "129 mmol/L  (corrected Na⁺ = 137)"),
        ("K⁺",    "5.8 mmol/L"),
        ("Glucose","28.4 mmol/L"),
        ("Anion Gap","25 mmol/L  (normal 8–12)"),
    ], LIGHTBLUE))
    story.append(Spacer(1, 4*mm))

    story.append(Paragraph("Urine Dipstick:", h3))
    story.append(info_box([
        ("Glucose", "4+ "),
        ("Ketones", "3+"),
        ("Protein",  "Trace"),
    ], LIGHTGREY))
    story.append(Spacer(1, 6*mm))

    story.append(section_header("MARKING SCHEME & MODEL ANSWERS", TEAL))
    story.append(Spacer(1, 3*mm))

    dka_marks = [
        ("History: Name 4 key points to elicit",
         4,
         "Duration of polyuria/polydipsia; vomiting/abdominal pain onset; insulin adherence (doses missed); "
         "precipitating illness (infection, stress); previous DKA episodes; current medications/insulin regimen."),

        ("ABG Interpretation: Identify the acid-base disorder",
         3,
         "High anion-gap metabolic acidosis (HAGMA). pH 7.18 = acidaemia. Low HCO₃⁻ = metabolic. "
         "Elevated anion gap (25) = accumulation of unmeasured anions (ketoacids). Low PaCO₂ = "
         "appropriate respiratory compensation (Kussmaul breathing). Formula: Expected PaCO₂ = "
         "1.5×HCO₃⁻ + 8 ± 2 = 18.5 — matches actual, confirming appropriate compensation."),

        ("Diagnosis + diagnostic criteria",
         3,
         "Diagnosis: Diabetic Ketoacidosis (DKA). Criteria: (1) Blood glucose >11 mmol/L (or known diabetes); "
         "(2) Ketonaemia ≥3 mmol/L or ketonuria ≥2+ on dipstick; (3) Bicarbonate <15 mmol/L and/or pH <7.3. "
         "This case is severe DKA (pH <7.1, HCO₃⁻ <5)."),

        ("List 6 immediate investigations",
         3,
         "FBC (WBC elevated in DKA even without infection); UECs (U&E — K⁺ critical); VBG/ABG serial; "
         "serum/capillary ketones; serum osmolality; urine MC&S; blood cultures (if febrile); ECG (hyperkalaemia); "
         "CXR (infection trigger); HbA1c; CRP; blood glucose hourly monitoring."),

        ("Initial resuscitation: First hour management",
         5,
         "IV access ×2, continuous cardiac monitoring, urinary catheter. "
         "Fluids: 1 L 0.9% NaCl over first 30–60 min (if systolic <90), then reassess. "
         "Do NOT start insulin until K⁺ >3.5 mmol/L — risk of fatal hypokalaemia. "
         "Potassium replacement: if K⁺ 3.5–5.5 mmol/L, add 40 mmol KCl to each litre; "
         "if K⁺ >5.5, hold K⁺ replacement. "
         "Fixed-rate insulin infusion: 0.1 units/kg/hr (e.g. 5 units/hr for 50 kg) once K⁺ corrected."),

        ("Fluid replacement plan hours 2–12",
         2,
         "1 L 0.9% NaCl over 2 hrs → 1 L over 4 hrs → 1 L over 4 hrs. "
         "Switch to 0.45% NaCl if corrected Na⁺ rising. Add 10% dextrose (125 mL/hr) when glucose "
         "falls to 14 mmol/L to prevent hypoglycaemia while continuing insulin to clear ketones."),

        ("Monitoring parameters — name 5",
         2,
         "Hourly: CBG, K⁺ (VBG), urine output, GCS, HR, BP. "
         "2-hourly: ketones (target fall ≥0.5 mmol/L/hr or urine ketones clearing). "
         "4-hourly: U&E, bicarbonate. "
         "Continuous: ECG, SpO₂. Resolution criteria: pH >7.3, HCO₃⁻ >18, ketones <0.3 mmol/L."),

        ("Name 3 complications of DKA or its treatment",
         3,
         "Cerebral oedema (most feared — especially in children/young adults — caused by rapid fluid shifts); "
         "Hypokalaemia (insulin drives K⁺ intracellularly — fatal arrhythmia risk); "
         "Hypoglycaemia (if dextrose not added when glucose drops); "
         "Aspiration pneumonia (gastroparesis, reduced GCS — insert NGT if GCS ≤8); "
         "Acute kidney injury (pre-renal from dehydration); "
         "Hyperchloraemic acidosis (from large-volume 0.9% NaCl)."),
    ]
    story.append(marking_table(dka_marks))
    story.append(Spacer(1, 4*mm))

    story.append(section_header("EXAMINER VIVA QUESTIONS — DKA", NAVY))
    story.append(Spacer(1, 3*mm))
    viva_dka = [
        ("Q: Why is insulin held until K⁺ >3.5 mmol/L?",
         "Insulin activates Na⁺/K⁺-ATPase, driving K⁺ into cells. Despite a high serum K⁺ in DKA, "
         "total body potassium is depleted (osmotic diuresis). Starting insulin in a patient who is "
         "already hypokalaemic causes precipitous drop in serum K⁺ → fatal cardiac arrhythmia."),
        ("Q: What is Kussmaul breathing and why does it occur?",
         "Deep, rapid, laboured respirations driven by metabolic acidosis. The respiratory centre "
         "attempts to compensate by blowing off CO₂, raising pH. Seen when pH <7.2."),
        ("Q: What precipitates DKA?",
         "The 5 I's: Infection (most common); Insulin omission; Infarction (MI, stroke); "
         "Inflammation (pancreatitis); Idiopathic (new-onset T1DM)."),
    ]
    for q, a in viva_dka:
        story.append(Paragraph(q, mark_q))
        story.append(Paragraph(a, mark_a))
    story.append(PageBreak())

    # ══════════════════════════════════════════════════════════════════════════
    # ══════  CASE 2: BLUNT ABDOMINAL TRAUMA  ═════════════════════════════════
    # ══════════════════════════════════════════════════════════════════════════
    story.append(section_header("OSCE STATION 2  —  Blunt Abdominal Trauma", NAVY))
    story.append(Spacer(1, 4*mm))
    story.append(info_box([
        ("Station type",  "History, Examination, Investigations & Management"),
        ("Time allowed",  "10 minutes"),
        ("Total marks",   "25"),
        ("Level",         "Clinical year medical students"),
        ("Competencies",  "Trauma assessment (ATLS), FAST exam, haemorrhagic shock, surgical decision-making"),
    ]))
    story.append(Spacer(1, 5*mm))

    story.append(section_header("CANDIDATE INSTRUCTIONS", TEAL))
    story.append(Spacer(1, 3*mm))
    story.append(Paragraph(
        "A 34-year-old male, <b>Ahmed</b>, is brought to the Emergency Department by ambulance after a "
        "<b>high-speed motor vehicle collision</b>. He was the restrained driver. The airbag deployed. "
        "He complains of <b>severe left-sided abdominal pain</b> and left shoulder tip pain. "
        "He is conscious but agitated. Paramedics have placed two large-bore IVs and given 500 mL 0.9% NaCl.",
        body))
    story.append(Spacer(1, 3*mm))

    story.append(Paragraph("Vital Signs:", h3))
    story.append(info_box([
        ("HR",          "118 bpm"),
        ("BP",          "88/56 mmHg"),
        ("RR",          "24 breaths/min"),
        ("SpO₂",        "96% on 15 L O₂ via NRB mask"),
        ("Temperature", "36.8 °C"),
        ("GCS",         "14/15"),
    ], LIGHTGREY))
    story.append(Spacer(1, 4*mm))

    story.append(Paragraph("Examination Findings:", h3))
    for finding in [
        "Abdomen: Distended, guarding in left upper quadrant and periumbilical region",
        "Left flank bruising (seatbelt sign)",
        "No bowel sounds",
        "Left shoulder tip pain on palpation",
        "No external wounds or evisceration",
        "Pelvis: Stable on compression",
    ]:
        story.append(Paragraph(f"• {finding}", bullet))
    story.append(Spacer(1, 3*mm))

    story.append(Paragraph("Tasks:", h3))
    for t_item in [
        "1. Describe your primary survey (ABCDE) for this patient",
        "2. Interpret the haemodynamic status and classify the shock",
        "3. State the diagnosis and explain the left shoulder tip pain",
        "4. Describe the role and findings of the FAST examination",
        "5. Outline your management plan: operative vs non-operative",
        "6. List investigations required",
    ]:
        story.append(Paragraph(f"• {t_item}", bullet))
    story.append(Spacer(1, 5*mm))

    story.append(section_header("MARKING SCHEME & MODEL ANSWERS", TEAL))
    story.append(Spacer(1, 3*mm))

    bat_marks = [
        ("Primary Survey: ABCDE approach — outline each step",
         5,
         "A - Airway: Patent; patient speaking — secure with cervical spine immobilisation. "
         "B - Breathing: RR 24, SpO₂ 96% — continue high-flow O₂ 15L NRB; check for tension pneumothorax, haemothorax. "
         "C - Circulation: HR 118, BP 88/56 = haemorrhagic shock. Two large-bore IVs already in; "
         "initiate massive transfusion protocol. "
         "D - Disability: GCS 14 — agitated but responding. Pupils equal and reactive. "
         "E - Exposure: Full exposure with log-roll maintaining c-spine; look for bruising, wounds, pelvis stability."),

        ("Classify the haemorrhagic shock",
         2,
         "Class III Haemorrhagic Shock: estimated blood loss 30–40% (1500–2000 mL). "
         "Features: HR >120 (borderline), BP <90, RR 20–30, GCS change, confused/agitated. "
         "Requires immediate blood transfusion (O-negative if type not known) + surgical haemostasis."),

        ("Diagnosis and explanation of left shoulder tip pain",
         3,
         "Diagnosis: <b>Splenic laceration</b> with haemoperitoneum (most likely given left-sided abdominal "
         "pain + seatbelt sign + haemodynamic instability). "
         "Shoulder tip pain (Kehr's sign): Blood or fluid tracking subphrenic irritates the left hemidiaphragm. "
         "The phrenic nerve (C3–C5) refers pain to the left shoulder."),

        ("FAST examination: describe and interpret findings",
         4,
         "FAST (Focused Assessment with Sonography in Trauma): bedside point-of-care ultrasound. "
         "4 windows: (1) Right upper quadrant (hepatorenal/Morison's pouch); (2) Left upper quadrant "
         "(splenorenal); (3) Pelvic (retrovesical/rectovesical); (4) Pericardial (cardiac). "
         "Extended eFAST adds bilateral lung views for pneumo/haemothorax. "
         "Expected finding: free fluid (anechoic/black) in LUQ (splenorenal recess) confirming haemoperitoneum. "
         "Positive FAST in haemodynamically unstable patient = indication for emergency laparotomy."),

        ("Management: operative vs non-operative decision",
         5,
         "Haemodynamically UNSTABLE (BP <90 despite resuscitation) + positive FAST = "
         "Emergency laparotomy WITHOUT CT scan. Goal: surgical haemostasis. "
         "Activate trauma surgery and theatre immediately. "
         "Massive transfusion protocol: 1:1:1 ratio (PRBCs:FFP:Platelets). "
         "Permissive hypotension: target systolic 80–90 mmHg until surgical control (limit crystalloids). "
         "Damage control surgery (DCS): control haemorrhage and contamination, temporary abdominal closure, "
         "resuscitate in ICU, definitive repair 24–48 hrs later. "
         "Non-operative management (NOM) reserved for haemodynamically STABLE patients with solid organ injury."),

        ("Investigations required — list 6",
         3,
         "FBC (Hb, platelets), coagulation screen (PT, APTT, fibrinogen), crossmatch (6 units PRBCs), "
         "UECs, LFTs, serum amylase/lipase (pancreatic injury), ABG (base deficit = severity of shock), "
         "blood group and screen, β-hCG (women of childbearing age), CXR, pelvic XR, "
         "CT abdomen/pelvis with IV contrast (only if haemodynamically stable — gold standard for IAI grading), "
         "FAST/eFAST (immediate bedside)."),

        ("What is the ATLS definition of haemoperitoneum requiring laparotomy?",
         3,
         "Positive FAST with haemodynamic instability despite initial resuscitation (≥2L crystalloid "
         "or 2 units blood). Additional: unequivocal peritoneal signs, evisceration, diaphragmatic rupture "
         "on imaging, evidence of hollow viscus perforation. "
         "CT grading of splenic injury (AAST Grade I–V) guides NOM in stable patients: "
         "Grade IV–V with vascular injury may require angioembolisation."),
    ]
    story.append(marking_table(bat_marks))
    story.append(Spacer(1, 4*mm))

    story.append(section_header("EXAMINER VIVA QUESTIONS — BLUNT ABDOMINAL TRAUMA", NAVY))
    story.append(Spacer(1, 3*mm))
    viva_bat = [
        ("Q: Why is CT contraindicated as the first step in this patient?",
         "The patient is haemodynamically unstable (BP 88/56). CT requires transfer to the scanner "
         "and takes time — in an exsanguinating patient this delay is fatal. CT is reserved for "
         "haemodynamically stable or stabilised patients. FAST at the bedside provides immediate "
         "confirmation of haemoperitoneum."),
        ("Q: What is damage control surgery?",
         "A staged operative strategy for physiologically compromised trauma patients. Stage 1: rapid "
         "control of bleeding and GI contamination (packing, clamps, resection without anastomosis). "
         "Stage 2: ICU resuscitation — correct the 'lethal triad' (hypothermia, acidosis, coagulopathy). "
         "Stage 3: definitive repair 24–48 hours later when physiology normalised."),
        ("Q: Name the most commonly injured solid organ in blunt abdominal trauma.",
         "The spleen is the most commonly injured organ in blunt abdominal trauma, "
         "followed by the liver and then the kidneys."),
    ]
    for q, a in viva_bat:
        story.append(Paragraph(q, mark_q))
        story.append(Paragraph(a, mark_a))
    story.append(PageBreak())

    # ══════════════════════════════════════════════════════════════════════════
    # ══════  CASE 3: SECONDARY SPONTANEOUS PNEUMOTHORAX IN COPD  ═════════════
    # ══════════════════════════════════════════════════════════════════════════
    story.append(section_header("OSCE STATION 3  —  Secondary Spontaneous Pneumothorax in COPD", NAVY))
    story.append(Spacer(1, 4*mm))
    story.append(info_box([
        ("Station type",  "History, Examination, Investigations & Management"),
        ("Time allowed",  "10 minutes"),
        ("Total marks",   "25"),
        ("Level",         "Clinical year medical students"),
        ("Competencies",  "Respiratory emergencies, pleural disease, chest drain insertion, COPD management"),
    ]))
    story.append(Spacer(1, 5*mm))

    story.append(section_header("CANDIDATE INSTRUCTIONS", TEAL))
    story.append(Spacer(1, 3*mm))
    story.append(Paragraph(
        "A <b>67-year-old male</b>, <b>George</b>, presents to the Emergency Department with a "
        "<b>2-hour history of sudden-onset left-sided chest pain and worsening breathlessness</b>. "
        "He has a <b>30-pack-year smoking history</b> and a known diagnosis of <b>severe COPD</b> "
        "(FEV₁/FVC &lt;0.70, FEV₁ 42% predicted). He uses Tiotropium and a Salbutamol PRN inhaler. "
        "He is distressed and unable to complete sentences.",
        body))
    story.append(Spacer(1, 3*mm))

    story.append(Paragraph("Vital Signs:", h3))
    story.append(info_box([
        ("HR",          "110 bpm"),
        ("BP",          "138/82 mmHg"),
        ("RR",          "32 breaths/min"),
        ("SpO₂",        "82% on room air → 91% on 2 L/min O₂ via nasal prongs"),
        ("Temperature", "37.1 °C"),
        ("GCS",         "15/15"),
    ], LIGHTGREY))
    story.append(Spacer(1, 4*mm))

    story.append(Paragraph("Examination Findings:", h3))
    for finding in [
        "Trachea: Deviated slightly to the RIGHT",
        "Left chest: Reduced expansion, absent breath sounds, hyper-resonance to percussion",
        "Right chest: Scattered expiratory wheeze, reduced air entry (baseline COPD)",
        "No raised JVP at rest",
        "Pursed-lip breathing, accessory muscle use",
        "No cyanosis centrally",
    ]:
        story.append(Paragraph(f"• {finding}", bullet))
    story.append(Spacer(1, 3*mm))

    story.append(Paragraph("Tasks:", h3))
    for t_item in [
        "1. State your diagnosis and classify the type of pneumothorax",
        "2. Explain why this patient is at higher risk than a young healthy patient",
        "3. Describe your immediate management (step-by-step)",
        "4. Interpret the CXR findings (described below)",
        "5. Describe chest drain insertion — indications, size, technique",
        "6. Name complications of tube thoracostomy",
    ]:
        story.append(Paragraph(f"• {t_item}", bullet))
    story.append(Spacer(1, 3*mm))

    story.append(Paragraph("CXR Report (provided to candidate):", h3))
    story.append(Paragraph(
        "PA chest radiograph: Absent vascular markings in the left hemithorax. "
        "Visible visceral pleural edge in the left mid-zone. "
        "Left lung collapsed to approximately 40% of normal volume. "
        "Trachea deviates 5 mm to the right. Flattened left hemidiaphragm. "
        "Right lung: hyperinflated with flattened diaphragm (consistent with COPD). "
        "No pleural effusion. No rib fractures.",
        body))
    story.append(Spacer(1, 5*mm))

    story.append(section_header("MARKING SCHEME & MODEL ANSWERS", TEAL))
    story.append(Spacer(1, 3*mm))

    ssp_marks = [
        ("Diagnosis and classification",
         3,
         "Diagnosis: <b>Secondary Spontaneous Pneumothorax (SSP)</b> in the context of COPD. "
         "Classification: Spontaneous (no trauma/procedure). Secondary (underlying lung disease — COPD). "
         "Large pneumothorax (visible rim >2 cm on CXR / >40% lung collapse). "
         "COPD is the most common cause of SSP in the United States. "
         "Mechanism: rupture of sub-pleural blebs/bullae secondary to chronic airflow obstruction, "
         "emphysematous destruction, and increased intraalveolar pressure."),

        ("Why is this patient at higher risk than a young person with PSP?",
         3,
         "Poor pulmonary reserve: FEV₁ 42% — any further reduction in effective lung volume causes "
         "severe hypoxaemia. In primary spontaneous pneumothorax (PSP), the contralateral lung is "
         "normal and compensates; in SSP the contralateral lung is already diseased. "
         "Dyspnoea is nearly universal even with small SSP. "
         "Symptoms are unlikely to resolve spontaneously — intervention is almost always required. "
         "Risk of tension pneumothorax is higher due to intrinsic PEEP and gas trapping in COPD."),

        ("Immediate management (oxygen, positioning, decision-making)",
         4,
         "Sit patient upright. "
         "Controlled oxygen: target SpO₂ 88–92% (COPD patient — risk of hypercapnic respiratory failure "
         "with high-flow O₂ — do NOT use NRB mask; use 28% Venturi mask or 2 L/min nasal prongs). "
         "IV access, monitoring (SpO₂, ECG, ETCO₂ if available). "
         "Urgent CXR (already done). ABG — assess for hypercapnia (type 2 respiratory failure). "
         "Decision: Large SSP in COPD = tube thoracostomy (chest drain) is the standard of care "
         "(simple aspiration has higher failure rate in SSP). "
         "Do NOT wait and observe — symptomatic large SSP in COPD requires drain insertion."),

        ("Chest drain — indications, size, technique",
         5,
         "Indications for chest drain in SSP: large pneumothorax (>2 cm rim); all SSP regardless of size "
         "if symptomatic; bilateral pneumothorax; haemopneumothorax; ventilated patients. "
         "Drain size: 20–28 Fr (large bore) for secondary spontaneous pneumothorax (vs. 8–14 Fr Seldinger "
         "technique for PSP). "
         "Technique: Patient supine or slightly lateral. Identify the 'safe triangle' — bounded by: "
         "anterior border of latissimus dorsi, lateral border of pectoralis major, above the 5th "
         "intercostal space (nipple line), apex below the axilla. "
         "Infiltrate with 1% lidocaine. Blunt dissection through intercostal space (superior edge of rib "
         "to avoid neurovascular bundle). Insert drain, confirm with misting/fogging and swinging with "
         "respiration. Connect to underwater seal drain. CXR to confirm position."),

        ("CXR Interpretation",
         4,
         "Findings on this CXR: (1) Absent peripheral lung markings in left hemithorax; "
         "(2) Visible visceral pleural line; (3) Left lung collapsed to ~40% volume; "
         "(4) Tracheal deviation to the RIGHT — the pneumothorax is on the LEFT (push mechanism). "
         "Note: Tracheal deviation in a spontaneous pneumothorax without haemodynamic collapse does NOT "
         "necessarily indicate tension pneumothorax, but must be watched closely. "
         "The right-sided hyperinflation and flat diaphragm are consistent with background COPD. "
         "Classify as a large left SSP."),

        ("Name 4 complications of tube thoracostomy",
         3,
         "Incorrect placement (subcutaneous, intra-fissural, or too deep/shallow); "
         "Haemothorax from intercostal vessel injury (insert above the rib); "
         "Lung laceration or perforation; "
         "Infection → empyema (strict aseptic technique required); "
         "Re-expansion pulmonary oedema (if large pneumothorax deflated too rapidly); "
         "Drain blockage or kinking; Surgical emphysema."),

        ("When is surgery indicated for pneumothorax?",
         3,
         "Persistent air leak >5–7 days despite drain; "
         "Second ipsilateral pneumothorax; "
         "First contralateral pneumothorax; "
         "Bilateral simultaneous pneumothorax; "
         "Tension pneumothorax that recurs; "
         "Haemopneumothorax requiring drainage >1.5 L. "
         "Surgery: Video-Assisted Thoracoscopic Surgery (VATS) — bullectomy/blebectomy + pleurodesis "
         "or pleurectomy. In poor surgical candidates: chemical pleurodesis via chest drain (talc slurry)."),
    ]
    story.append(marking_table(ssp_marks))
    story.append(Spacer(1, 4*mm))

    story.append(section_header("EXAMINER VIVA QUESTIONS — SECONDARY SPONTANEOUS PNEUMOTHORAX", NAVY))
    story.append(Spacer(1, 3*mm))
    viva_ssp = [
        ("Q: Why should you NOT give high-flow oxygen via NRB mask to a COPD patient?",
         "In COPD with chronic CO₂ retention, the hypoxic drive maintains respiratory effort. "
         "Uncontrolled high-flow O₂ corrects hypoxaemia but removes the hypoxic drive, "
         "leading to hypercapnic respiratory failure and CO₂ narcosis. "
         "Target SpO₂ 88–92%, using a Venturi mask at 28–35% FiO₂ or low-flow nasal prongs."),
        ("Q: How do you differentiate tension pneumothorax from a large simple pneumothorax clinically?",
         "Tension pneumothorax: life-threatening — presents with rapid deterioration, severe hypoxia, "
         "hypotension, tachycardia, absent breath sounds, tracheal deviation AWAY from the affected side, "
         "and distended neck veins (elevated JVP). It is a clinical diagnosis — do NOT wait for CXR. "
         "Treat immediately with needle decompression (2nd ICS, MCL) then chest drain. "
         "Large simple pneumothorax: patient may be distressed but haemodynamically stable."),
        ("Q: What is the difference between primary and secondary spontaneous pneumothorax?",
         "Primary (PSP): occurs in the absence of clinically apparent underlying lung disease; "
         "typically in tall, young male smokers; blebs rupture. Management may involve observation "
         "or simple aspiration for small asymptomatic PSP. "
         "Secondary (SSP): occurs on a background of known pulmonary disease (COPD, cystic fibrosis, "
         "TB, malignancy). Always symptomatic due to poor reserve. Requires more aggressive intervention "
         "(tube thoracostomy) and has higher morbidity and recurrence rates."),
    ]
    for q, a in viva_ssp:
        story.append(Paragraph(q, mark_q))
        story.append(Paragraph(a, mark_a))

    # Footer note
    story.append(Spacer(1, 8*mm))
    story.append(HRFlowable(width="100%", thickness=1, color=TEAL))
    story.append(Spacer(1, 2*mm))
    story.append(Paragraph(
        "<i>References: Goldman-Cecil Medicine (Elsevier, 2024); Rosen's Emergency Medicine "
        "(9th ed.); Tintinalli's Emergency Medicine (9th ed.); Murray & Nadel's Respiratory Medicine; "
        "Fischer's Mastery of Surgery (8th ed.). OSCE cases prepared for clinical year medical students. "
        "June 2026.</i>",
        ParagraphStyle("footer", fontSize=7.5, textColor=colors.grey,
                        fontName="Helvetica-Oblique", alignment=TA_CENTER, leading=11)))

    doc.build(story)
    print(f"PDF saved to: {OUTPUT_PATH}")

if __name__ == "__main__":
    build_pdf()
Running Command

cd /home/daytona/workspace/osce-cases && python generate_osce.py 2>&1

Running Command

ls -lh /home/daytona/workspace/osce-cases/OSCE_Cases.pdf

Your PDF is ready:

OSCE Cases

PDF Document · PDF


Here is a summary of what is inside each station:

Station 1 — Diabetic Ketoacidosis (DKA)

Patient: 19-year-old female, T1DM, 2 days of vomiting, abdominal pain, missed insulin.
  • ABG showing high-anion-gap metabolic acidosis (pH 7.18, HCO₃⁻ 7, AG 25) + ketones 3+ on dipstick
  • 25 marks across: focused history, ABG interpretation, diagnostic criteria, investigations, fluid/insulin/K⁺ management protocol, monitoring, and complications (cerebral oedema, hypokalaemia, aspiration)
  • Viva: why insulin is held until K⁺ >3.5, Kussmaul breathing, the 5 precipitants of DKA

Station 2 — Blunt Abdominal Trauma

Patient: 34-year-old male, high-speed MVA, BP 88/56, left abdominal guarding and seatbelt sign.
  • 25 marks across: ATLS primary survey (ABCDE), haemorrhagic shock classification (Class III), splenic laceration diagnosis + Kehr's sign, FAST examination, emergent laparotomy decision-making, massive transfusion protocol, damage control surgery
  • Viva: why CT is contraindicated in the unstable patient, damage control surgery definition, most commonly injured organ in blunt trauma

Station 3 — Secondary Spontaneous Pneumothorax in COPD

Patient: 67-year-old male, severe COPD (FEV₁ 42%), sudden left chest pain, SpO₂ 82%, absent left breath sounds + hyper-resonance, trachea deviated right.
  • 25 marks across: PSP vs SSP classification, why COPD patients tolerate pneumothorax worse, controlled O₂ targets (88–92%), chest drain indications/safe triangle technique/size (20–28 Fr), CXR interpretation, tube thoracostomy complications, surgical indications (VATS)
  • Viva: why NRB mask is dangerous in COPD, tension vs large simple pneumothorax, PSP vs SSP differences
This is a shared conversation. Sign in to Orris to start your own chat.