canyou make a exam after clicking answer it should reveal answer
cd /tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414 && python3 -c " import pdfplumber with pdfplumber.open('Immunology _260703_164214.pdf') as pdf: print(f'Total pages: {len(pdf.pages)}') for i, page in enumerate(pdf.pages[:5]): print(f'--- PAGE {i+1} ---') print(page.extract_text()) print() "
python3 -c " import pdfplumber with pdfplumber.open('/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf') as pdf: # Sample pages to find answer key for i in [10, 20, 30, 40, 50, 100, 200, 300, 400, 450, 480, 490, 491]: print(f'--- PAGE {i+1} ---') print(pdf.pages[i].extract_text()[:300]) print() "
python3 << 'EOF' import pdfplumber, re, json pdf_path = '/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf' # Extract all text from every page all_text = [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: t = page.extract_text() if t: all_text.append(t) full_text = '\n'.join(all_text) # Save raw text for inspection with open('/tmp/workspace/immunology_raw.txt', 'w') as f: f.write(full_text) print(f"Total chars: {len(full_text)}") print("First 2000 chars:") print(full_text[:2000]) EOF
python3 << 'EOF' import re, json with open('/tmp/workspace/immunology_raw.txt') as f: raw = f.read() # The correct answer is marked by doubling both the letter and the paren: # e.g. "AA)) Anti-toxins" or "BB)) Something" # First, let's normalize doubled text (scrambled OCR artifact for bold) # e.g. "77.. CCyyttoottooxxiicc TT--cceellllss::" -> "7. Cytotoxic T-cells:" def undouble(s): """Remove character doubling from OCR bold artifacts.""" result = [] i = 0 while i < len(s): if i + 1 < len(s) and s[i] == s[i+1] and s[i] not in (' ', '\n', '\t'): result.append(s[i]) i += 2 else: result.append(s[i]) i += 1 return ''.join(result) # Find all answer lines that have doubled letter+paren pattern indicating correct answer # Pattern: doubled letter like AA)) or BB)) etc. correct_answer_pattern = re.compile(r'\b([A-E])\1\)\s*\)', ) # nope # Actually the pattern is: "AA)) text" -- two same letters + two close-parens correct_answer_pattern = re.compile(r'([A-E])\1\)\)') # Let's find these in raw text samples = correct_answer_pattern.findall(raw) print(f"Found {len(samples)} doubled-answer markers") print("Sample doubled answers:", samples[:20]) # Show context around a few for m in re.finditer(r'([A-E])\1\)\)', raw): start = max(0, m.start()-100) end = min(len(raw), m.end()+100) print(repr(raw[start:end])) print('---') if samples.index(m.group(1)) > 5: break EOF
python3 << 'PYEOF' import re, json with open('/tmp/workspace/immunology_raw.txt') as f: raw = f.read() # ─── helpers ─────────────────────────────────────────────────────────────────── def undouble(s): """Remove OCR-bold character-doubling: 'AA))' -> 'A)' 'CCyyttoottooxxiicc' -> 'Cytotoxic'""" result = [] i = 0 while i < len(s): if i + 1 < len(s) and s[i] == s[i+1] and s[i] not in (' ', '\n', '\t'): result.append(s[i]) i += 2 else: result.append(s[i]) i += 1 return ''.join(result) # ─── normalise raw text ──────────────────────────────────────────────────────── # 1. Collapse lines so multi-line questions join up # 2. Keep paragraph structure for question blocks lines = raw.split('\n') # Join continuation lines (lines that don't start a question or option) q_start = re.compile(r'^\s*\d+[\.\)]\s') opt_start = re.compile(r'^\s*[A-E]\)') joined = [] for line in lines: line = line.rstrip() if not line: joined.append('') continue if q_start.match(line) or opt_start.match(line): joined.append(line) else: if joined: joined[-1] += ' ' + line else: joined.append(line) text = '\n'.join(joined) # ─── parse questions ─────────────────────────────────────────────────────────── # A "question block" = Q text + A-E options + one correct (doubled) option # Strategy: split by question numbers # Find all question starts: "123. Some text" q_pat = re.compile(r'(?m)^(\d+)[\.\)]\s+(.+?)(?=\n\d+[\.\)]\s|\Z)', re.DOTALL) # We'll build a list of {num, text, options: {A:.., B:.., C:.., D:.., E:..}, answer: X} questions = [] # Use a simpler block approach: scan line by line current_num = None current_q = [] current_opts = {} current_ans = None def save_q(): global current_num, current_q, current_opts, current_ans if current_num and current_q and len(current_opts) >= 2: questions.append({ 'num': current_num, 'q': undouble(' '.join(current_q).strip()), 'opts': {k: undouble(v.strip()) for k,v in current_opts.items()}, 'ans': current_ans }) current_num = None current_q = [] current_opts = {} current_ans = None num_re = re.compile(r'^(\d+)[\.\)]\s+(.*)') opt_re = re.compile(r'^([A-E])\)\s+(.*)') # doubled option like "AA)) ..." or "BB)) ..." dopt_re = re.compile(r'^([A-E])\1\)\)\s*(.*)') lines2 = raw.split('\n') for raw_line in lines2: line = raw_line.strip() if not line: continue # check doubled option first (correct answer) dm = dopt_re.match(line) if dm: letter = dm.group(1) text_part = undouble(dm.group(2).strip()) current_opts[letter] = text_part current_ans = letter continue # regular option om = opt_re.match(line) if om: letter = om.group(1) text_part = om.group(2).strip() current_opts[letter] = text_part continue # new question number nm = num_re.match(line) if nm: new_num = int(nm.group(1)) rest = nm.group(2).strip() # Only start a new Q if the number is plausibly sequential or new if current_num is not None and new_num != current_num: save_q() if current_num is None or new_num != current_num: current_num = new_num current_q = [rest] if rest else [] else: # same number appearing again (duplicate page artifact) – skip pass continue # continuation of question text if current_num is not None and not current_opts: current_q.append(line) save_q() print(f"Parsed {len(questions)} questions") # Show how many have answers with_ans = [q for q in questions if q['ans']] print(f"Questions with identified answer: {len(with_ans)}") # Show first 5 for q in questions[:5]: print(q) print('...') for q in questions[-3:]: print(q) # Save with open('/tmp/workspace/immunology_questions.json', 'w') as f: json.dump(questions, f, indent=2) PYEOF
python3 << 'PYEOF' # Let's look at what the "answer" pages look like - they seem to have just numbers and answers # Let's look at a few pages that seem to be answer keys import pdfplumber with pdfplumber.open('/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf') as pdf: # Page layout: questions seem to span 2 pages each (question + its answer) # Let's look at pages where doubled answers appear for i in range(5, 15): t = pdf.pages[i].extract_text() if t: print(f'=== PAGE {i+1} ===') print(t[:500]) print() PYEOF
python3 << 'PYEOF' import json, re with open('/tmp/workspace/immunology_questions.json') as f: questions = json.load(f) # Filter questions that have at least 2 options valid_qs = [q for q in questions if len(q['opts']) >= 2] print(f"Valid questions: {len(valid_qs)}") print(f"With answers: {len([q for q in valid_qs if q['ans']])}") # De-duplicate: keep only unique question numbers (first occurrence) seen = {} deduped = [] for q in valid_qs: n = q['num'] if n not in seen: seen[n] = True deduped.append(q) print(f"After dedup: {len(deduped)} questions") print(f"With answers after dedup: {len([q for q in deduped if q['ans']])}") with open('/tmp/workspace/immunology_questions_clean.json', 'w') as f: json.dump(deduped, f, indent=2) PYEOF
python3 << 'PYEOF' import json, html as htmllib with open('/tmp/workspace/immunology_questions_clean.json') as f: questions = json.load(f) def esc(s): return htmllib.escape(str(s)) # Build question cards HTML cards_html = [] for idx, q in enumerate(questions): opts = q['opts'] ans = q['ans'] opts_html = [] for letter in ['A','B','C','D','E']: if letter not in opts: continue is_correct = (letter == ans) correct_cls = ' data-correct="true"' if is_correct else '' opts_html.append( f'<li class="option" {correct_cls} data-letter="{letter}" onclick="chooseOption(this)">' f'<span class="opt-letter">{letter}</span>' f'<span class="opt-text">{esc(opts[letter])}</span>' f'</li>' ) opts_str = '\n'.join(opts_html) # Answer reveal section if ans and ans in opts: ans_text = esc(opts[ans]) reveal_html = ( f'<div class="reveal hidden" id="reveal-{idx}">' f'<span class="correct-badge">✓ Correct Answer: {ans}) {ans_text}</span>' f'</div>' ) else: reveal_html = ( f'<div class="reveal hidden" id="reveal-{idx}">' f'<span class="no-ans-badge">Answer not marked in PDF for this question</span>' f'</div>' ) answered_cls = '' if ans else ' no-answer' cards_html.append(f''' <div class="card{answered_cls}" id="card-{idx}"> <div class="q-header"> <span class="q-num">Q{q["num"]}</span> {"<span class='has-ans'>●</span>" if ans else ""} </div> <p class="q-text">{esc(q["q"])}</p> <ul class="options">{opts_str}</ul> {reveal_html} <button class="reveal-btn" onclick="revealAnswer({idx})">Show Answer</button> </div>''') all_cards = '\n'.join(cards_html) total = len(questions) with_ans = len([q for q in questions if q['ans']]) html_content = f'''<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Immunology MCQ Exam</title> <style> :root {{ --bg: #0f1117; --card-bg: #1a1d27; --card-border: #2a2d3e; --accent: #7c6af7; --accent2: #5eead4; --correct: #22c55e; --wrong: #ef4444; --text: #e2e8f0; --muted: #94a3b8; --opt-hover: #252839; --opt-selected: #2a2d3e; }} * {{ box-sizing: border-box; margin: 0; padding: 0; }} body {{ font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; padding: 0 0 60px 0; }} header {{ background: linear-gradient(135deg, #1e1b4b, #312e81); padding: 32px 24px 24px; text-align: center; border-bottom: 2px solid var(--accent); position: sticky; top: 0; z-index: 100; }} header h1 {{ font-size: 1.8rem; color: #c7d2fe; letter-spacing: 0.05em; }} header p {{ color: var(--muted); margin-top: 6px; font-size: 0.95rem; }} .stats {{ display: flex; gap: 16px; justify-content: center; margin-top: 16px; flex-wrap: wrap; }} .stat-pill {{ background: rgba(255,255,255,0.08); border-radius: 20px; padding: 6px 16px; font-size: 0.85rem; color: #a5b4fc; }} .controls {{ max-width: 900px; margin: 20px auto 0; padding: 0 16px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }} .search-box {{ flex: 1; min-width: 200px; padding: 10px 16px; border-radius: 8px; border: 1px solid var(--card-border); background: var(--card-bg); color: var(--text); font-size: 0.95rem; }} .search-box::placeholder {{ color: var(--muted); }} .filter-btn {{ padding: 9px 18px; border-radius: 8px; border: 1px solid var(--card-border); background: var(--card-bg); color: var(--muted); cursor: pointer; font-size: 0.85rem; transition: all .2s; }} .filter-btn:hover, .filter-btn.active {{ background: var(--accent); color: white; border-color: var(--accent); }} #progress-bar-wrap {{ max-width: 900px; margin: 12px auto 0; padding: 0 16px; }} #progress-bar {{ background: var(--card-border); border-radius: 8px; height: 8px; overflow: hidden; }} #progress-fill {{ height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2)); width: 0%; transition: width .4s; border-radius: 8px; }} #progress-label {{ text-align: right; font-size: 0.8rem; color: var(--muted); margin-top: 4px; }} .grid {{ max-width: 900px; margin: 20px auto 0; padding: 0 16px; display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 20px; }} .card {{ background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 14px; padding: 20px; transition: border-color .2s, transform .1s; position: relative; }} .card:hover {{ border-color: #4a4d6e; transform: translateY(-1px); }} .card.answered {{ border-color: var(--correct) !important; }} .card.wrong-ans {{ border-color: var(--wrong) !important; }} .q-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }} .q-num {{ font-size: 0.8rem; font-weight: 700; background: var(--accent); color: white; padding: 3px 10px; border-radius: 20px; }} .has-ans {{ color: var(--accent2); font-size: 0.75rem; title: "Answer available"; }} .q-text {{ font-size: 0.95rem; line-height: 1.6; color: #cbd5e1; margin-bottom: 14px; }} .options {{ list-style: none; display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }} .option {{ display: flex; align-items: flex-start; gap: 10px; padding: 10px 14px; border-radius: 8px; border: 1px solid var(--card-border); cursor: pointer; transition: all .15s; user-select: none; }} .option:hover {{ background: var(--opt-hover); border-color: #5a5d8e; }} .option.selected {{ background: var(--opt-selected); border-color: var(--accent); }} .option.show-correct {{ background: rgba(34,197,94,0.15); border-color: var(--correct); }} .option.show-wrong {{ background: rgba(239,68,68,0.12); border-color: var(--wrong); }} .opt-letter {{ font-weight: 700; font-size: 0.85rem; color: var(--accent); min-width: 18px; flex-shrink: 0; }} .opt-text {{ font-size: 0.88rem; line-height: 1.5; color: #d1d5db; }} .reveal {{ margin-bottom: 12px; padding: 10px 14px; border-radius: 8px; font-size: 0.875rem; font-weight: 600; }} .reveal.hidden {{ display: none; }} .correct-badge {{ color: var(--correct); background: rgba(34,197,94,0.1); padding: 8px 14px; border-radius: 8px; display: block; border-left: 3px solid var(--correct); }} .no-ans-badge {{ color: var(--muted); background: rgba(148,163,184,0.07); padding: 8px 14px; border-radius: 8px; display: block; border-left: 3px solid var(--muted); }} .reveal-btn {{ width: 100%; padding: 9px; border-radius: 8px; border: 1px solid var(--accent); background: transparent; color: var(--accent); font-size: 0.875rem; cursor: pointer; transition: all .15s; font-weight: 600; }} .reveal-btn:hover {{ background: var(--accent); color: white; }} .reveal-btn.revealed {{ background: rgba(124,106,247,0.12); color: #a78bfa; border-color: #a78bfa; cursor: default; }} .hidden {{ display: none !important; }} #no-results {{ text-align: center; padding: 60px; color: var(--muted); display: none; grid-column: 1/-1; }} .score-toast {{ position: fixed; bottom: 24px; right: 24px; background: var(--card-bg); border: 1px solid var(--accent); border-radius: 12px; padding: 14px 20px; font-size: 0.9rem; color: var(--text); box-shadow: 0 8px 32px rgba(0,0,0,0.4); z-index: 200; opacity: 0; transform: translateY(20px); transition: all .3s; }} .score-toast.show {{ opacity: 1; transform: translateY(0); }} </style> </head> <body> <header> <h1>🧬 Immunology MCQ Exam</h1> <p>Click any option to select, then click <strong>Show Answer</strong> to reveal the correct answer</p> <div class="stats"> <span class="stat-pill">📋 {total} Questions</span> <span class="stat-pill">✅ {with_ans} With Answers</span> <span class="stat-pill" id="answered-stat">🎯 0 Answered</span> <span class="stat-pill" id="correct-stat">✓ 0 Correct</span> </div> </header> <div class="controls"> <input type="text" class="search-box" id="search" placeholder="🔍 Search questions..." oninput="filterCards()"> <button class="filter-btn active" onclick="setFilter('all', this)">All</button> <button class="filter-btn" onclick="setFilter('answered', this)">Answered</button> <button class="filter-btn" onclick="setFilter('unanswered', this)">Unanswered</button> <button class="filter-btn" onclick="setFilter('has-answer', this)">Has Key ●</button> <button class="filter-btn" id="reveal-all-btn" onclick="revealAll()">Reveal All</button> </div> <div id="progress-bar-wrap"> <div id="progress-bar"><div id="progress-fill"></div></div> <div id="progress-label">0 / {total} answered</div> </div> <div class="grid" id="grid"> {all_cards} <div id="no-results">No questions match your search.</div> </div> <div class="score-toast" id="toast"></div> <script> const TOTAL = {total}; let answeredCount = 0; let correctCount = 0; let currentFilter = 'all'; let revealedAll = false; function chooseOption(el) {{ const card = el.closest('.card'); if (card.dataset.locked) return; card.querySelectorAll('.option').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); card.dataset.selected = el.dataset.letter; }} function revealAnswer(idx) {{ const card = document.getElementById('card-' + idx); const revealDiv = document.getElementById('reveal-' + idx); const btn = card.querySelector('.reveal-btn'); if (card.dataset.locked) return; card.dataset.locked = '1'; btn.classList.add('revealed'); btn.textContent = 'Answer Shown'; // Show the reveal div revealDiv.classList.remove('hidden'); // Highlight options const correctLetter = card.querySelector('[data-correct]')?.dataset.letter; const selectedLetter = card.dataset.selected; if (correctLetter) {{ card.querySelectorAll('.option').forEach(opt => {{ if (opt.dataset.letter === correctLetter) {{ opt.classList.add('show-correct'); }} else if (opt.dataset.letter === selectedLetter && selectedLetter !== correctLetter) {{ opt.classList.add('show-wrong'); }} }}); // Scoring if (!card.dataset.counted) {{ card.dataset.counted = '1'; answeredCount++; if (selectedLetter === correctLetter) {{ correctCount++; card.classList.add('answered'); showToast('✓ Correct!', 'correct'); }} else {{ card.classList.add('wrong-ans'); if (selectedLetter) showToast('✗ Wrong. Correct: ' + correctLetter, 'wrong'); }} updateStats(); }} }} else {{ // No answer key if (selectedLetter) {{ card.querySelectorAll('.option').forEach(opt => {{ if (opt.dataset.letter === selectedLetter) opt.classList.add('selected'); }}); }} if (!card.dataset.counted) {{ card.dataset.counted = '1'; answeredCount++; updateStats(); }} }} filterCards(); }} function updateStats() {{ document.getElementById('answered-stat').textContent = '🎯 ' + answeredCount + ' Answered'; document.getElementById('correct-stat').textContent = '✓ ' + correctCount + ' Correct'; const pct = (answeredCount / TOTAL * 100).toFixed(0); document.getElementById('progress-fill').style.width = pct + '%'; document.getElementById('progress-label').textContent = answeredCount + ' / ' + TOTAL + ' answered'; }} let toastTimer; function showToast(msg, type) {{ const t = document.getElementById('toast'); t.textContent = msg; t.style.borderColor = type === 'correct' ? '#22c55e' : '#ef4444'; t.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove('show'), 2000); }} function setFilter(f, btn) {{ currentFilter = f; document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); filterCards(); }} function filterCards() {{ const q = document.getElementById('search').value.toLowerCase(); let visible = 0; document.querySelectorAll('.card').forEach(card => {{ const text = card.querySelector('.q-text').textContent.toLowerCase(); const matchSearch = !q || text.includes(q); let matchFilter = true; if (currentFilter === 'answered') matchFilter = !!card.dataset.locked; if (currentFilter === 'unanswered') matchFilter = !card.dataset.locked; if (currentFilter === 'has-answer') matchFilter = !!card.querySelector('[data-correct]'); if (matchSearch && matchFilter) {{ card.style.display = ''; visible++; }} else {{ card.style.display = 'none'; }} }}); document.getElementById('no-results').style.display = visible === 0 ? 'block' : 'none'; }} function revealAll() {{ if (revealedAll) return; revealedAll = true; document.getElementById('reveal-all-btn').textContent = 'Revealed All'; document.querySelectorAll('.card').forEach((card, idx) => {{ if (!card.dataset.locked) revealAnswer(idx); }}); }} // Keyboard shortcut: press 1-5 to select A-E on focused card (future enhancement) </script> </body> </html>''' out_path = '/tmp/workspace/immunology_exam.html' with open(out_path, 'w', encoding='utf-8') as f: f.write(html_content) import os size_kb = os.path.getsize(out_path) // 1024 print(f"HTML written: {out_path} ({size_kb} KB)") print(f"Total questions: {len(questions)}") print(f"Questions with answer key: {with_ans}") PYEOF
immunology exam
HTML File · HTML
AA)) in text extraction)where is remaining qn
python3 << 'PYEOF' import pdfplumber # Let's look at a wider range of pages to understand answer pattern with pdfplumber.open('/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf') as pdf: total = len(pdf.pages) print(f"Total pages: {total}") # Sample every 20 pages for i in [0,1,2,3,4,5,6,7,8,9,10,15,20,25,30]: t = pdf.pages[i].extract_text() print(f"=== PAGE {i+1} ===") print(t[:400] if t else "(empty)") print() PYEOF
python3 << 'PYEOF' # The PDF layout is: # Page N: shows Q1, Q2 (partial), "3." # Page N+1: shows "1." "2." + Q3 full text + Q4... # The ANSWER is BOLDED in the PDF - and the text extractor renders bold as doubled chars # BUT many questions don't have bold answers showing up. # Let's check: maybe questions 14, 15, 18, 20, 21 etc have answers on other pages # that we're missing because they appear BEFORE the options # Let's extract ALL text per page and look at what the "1." "2." lines tell us import pdfplumber, re answer_lines = {} # q_num -> answer letter with pdfplumber.open('/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf') as pdf: for page_i, page in enumerate(pdf.pages): t = page.extract_text() if not t: continue lines = t.split('\n') for line in lines: line = line.strip() # Look for standalone "N." lines - these are question number stubs # and look for doubled option patterns anywhere on the page # Pattern: "NN.. " or "N." alone = just a number stub (answer placeholder) # What we need: a doubled letter+paren RIGHT AFTER a question number # Like "10. AA)) Anti-toxins" - but they're on same page as question pass # Better approach: use pdfplumber's char-level data to find BOLD text # Bold chars have different fontname typically with pdfplumber.open('/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf') as pdf: # Check page 5 (has Q10 with known answer AA))) page = pdf.pages[4] chars = page.chars # Print unique fontnames fonts = set(c['fontname'] for c in chars) print("Fonts on page 5:", fonts) # Show first 30 chars with font info for c in chars[:50]: print(f" char={repr(c['text'])} font={c['fontname']} size={c['size']:.1f}") PYEOF
python3 << 'PYEOF' # Font is same for all - so bold is encoded differently. Let's check color/rendering mode or size import pdfplumber with pdfplumber.open('/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf') as pdf: # Page 5 has "AA)) AAnnttii--ttooxxiinnss" - let's look at all char properties page = pdf.pages[4] chars = page.chars # Look at unique properties sample = chars[0] print("All char properties:", list(sample.keys())) # Check colors and sizes - find doubled chars text = page.extract_text() print("\nPage 5 text:") print(text) print() # Find the AA)) chars specifically - check their properties # The doubled text "AA))" - look for consecutive same chars for i in range(len(chars)-1): if chars[i]['text'] == chars[i+1]['text'] and chars[i]['text'] not in (' ', '\n'): c = chars[i] print(f"DOUBLED char '{c['text']}': size={c['size']}, color={c.get('non_stroking_color')}, stroking={c.get('stroking_color')}, render={c.get('rendering_mode')}, upright={c.get('upright')}") break # Check ALL unique colors colors = set() for c in chars: colors.add(str(c.get('non_stroking_color'))) print("\nUnique colors:", colors) PYEOF
python3 << 'PYEOF' import pdfplumber, re, json pdf_path = '/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf' ANSWER_COLOR = (0.78039217, 0.40392157, 0.65098041) def is_answer_color(color): if color is None: return False if isinstance(color, (list, tuple)) and len(color) == 3: return abs(color[0] - 0.78039217) < 0.01 return False # Extract all colored (answer) words per page # Strategy: group colored chars into words/lines, then extract question number + answer letter all_answers = {} # q_num -> answer_letter with pdfplumber.open(pdf_path) as pdf: for page_i, page in enumerate(pdf.pages): chars = page.chars # Get colored chars colored = [c for c in chars if is_answer_color(c.get('non_stroking_color'))] if not colored: continue # Reconstruct colored text (group by proximity) colored_text = ''.join(c['text'] for c in colored) # Also get all text on page to find context page_text = page.extract_text() or '' # Show colored chars for debugging first few pages if page_i < 10: print(f"Page {page_i+1} colored text: {repr(colored_text[:200])}") print(f"\nTotal pages with colored text found: checking...") # Count pages with colored chars count = 0 with pdfplumber.open(pdf_path) as pdf: for page_i, page in enumerate(pdf.pages): colored = [c for c in page.chars if is_answer_color(c.get('non_stroking_color'))] if colored: count += 1 print(f"Pages with answer color: {count}") PYEOF
python3 << 'PYEOF' import pdfplumber, re, json pdf_path = '/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf' ANSWER_COLOR = (0.78039217, 0.40392157, 0.65098041) def is_answer_color(color): if color is None: return False if isinstance(color, (list,tuple)) and len(color)==3: return abs(color[0]-0.78039217)<0.02 return False # Extract per page: all normal text lines + colored option lines # We'll build a merged structure: for each page get: # - all text (for question/option context) # - colored segments (these are correct answers) # On each page, colored text looks like: "X) Option text" # We match this to options in that same page opt_re = re.compile(r'^([A-E])\)\s+(.+)', re.DOTALL) # Build full question list with answers using char-level color detection # Page by page: extract (question_number, options, correct_answer_letter) page_data = [] # list of {page, questions_seen, colored_options} with pdfplumber.open(pdf_path) as pdf: for page_i, page in enumerate(pdf.pages): chars = page.chars if not chars: continue # Separate chars by color normal_chars = [] colored_chars = [] for c in chars: col = c.get('non_stroking_color') if is_answer_color(col): colored_chars.append(c) else: normal_chars.append(c) # Reconstruct lines for normal text # Group chars by y position (same line = within 3pt) def chars_to_lines(char_list): if not char_list: return [] sorted_chars = sorted(char_list, key=lambda c: (round(c['top'],0), c['x0'])) lines = [] current_line = [] current_y = None for c in sorted_chars: y = round(c['top'], 0) if current_y is None or abs(y - current_y) < 5: current_line.append(c) current_y = y else: lines.append(''.join(ch['text'] for ch in current_line)) current_line = [c] current_y = y if current_line: lines.append(''.join(ch['text'] for ch in current_line)) return [l.strip() for l in lines if l.strip()] normal_lines = chars_to_lines(chars) # full page text colored_lines = chars_to_lines(colored_chars) page_data.append({ 'page': page_i+1, 'normal': normal_lines, 'colored': colored_lines }) # Save page data for inspection print(f"Pages processed: {len(page_data)}") print("\nPage 1 colored lines:", page_data[0]['colored']) print("Page 1 normal lines (first 20):", page_data[0]['normal'][:20]) PYEOF
python3 << 'PYEOF' import pdfplumber, re, json pdf_path = '/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf' ANSWER_COLOR_R = 0.78039217 def is_answer_color(color): if color is None: return False if isinstance(color,(list,tuple)) and len(color)==3: return abs(color[0]-ANSWER_COLOR_R)<0.02 return False def chars_to_lines(char_list): if not char_list: return [] sorted_chars = sorted(char_list, key=lambda c: (round(c['top'],0), c['x0'])) lines, current_line, current_y = [], [], None for c in sorted_chars: y = round(c['top'],0) if current_y is None or abs(y-current_y)<5: current_line.append(c); current_y=y else: lines.append(''.join(ch['text'] for ch in current_line)) current_line=[c]; current_y=y if current_line: lines.append(''.join(ch['text'] for ch in current_line)) return [l.strip() for l in lines if l.strip()] def undouble(s): result=[]; i=0 while i<len(s): if i+1<len(s) and s[i]==s[i+1] and s[i] not in (' ','\n','\t'): result.append(s[i]); i+=2 else: result.append(s[i]); i+=1 return ''.join(result) # Parse all pages q_num_re = re.compile(r'^(\d+)[.\)](.+)', re.DOTALL) opt_re = re.compile(r'^([A-E])[.)] ?(.*)', re.DOTALL) # We'll accumulate all questions globally questions = {} # num -> {q, opts:{A:..,B:..}, ans:None} ans_map = {} # (option_text_normalized) -> letter -- per question with pdfplumber.open(pdf_path) as pdf: for page_i, page in enumerate(pdf.pages): chars = page.chars if not chars: continue all_lines = chars_to_lines(chars) colored_lines = chars_to_lines([c for c in chars if is_answer_color(c.get('non_stroking_color'))]) # Parse questions from this page current_num = None current_q_text = [] current_opts = {} def flush(): nonlocal current_num, current_q_text, current_opts if current_num and (current_q_text or current_opts): if current_num not in questions: questions[current_num] = { 'num': current_num, 'q': undouble(' '.join(current_q_text).strip()), 'opts': {}, 'ans': None } # Merge opts for k,v in current_opts.items(): if k not in questions[current_num]['opts']: questions[current_num]['opts'][k] = undouble(v.strip()) current_num=None; current_q_text=[]; current_opts={} for line in all_lines: nm = q_num_re.match(line) om = opt_re.match(line) if nm: flush() current_num = int(nm.group(1)) rest = nm.group(2).strip() current_q_text = [rest] if rest else [] elif om and current_num: letter = om.group(1) text = om.group(2).strip() current_opts[letter] = text elif current_num and not current_opts: current_q_text.append(line) flush() # Now match colored lines to answers # colored line like "D) Salivary amylase" or "E) Restrict viral replication" for cline in colored_lines: cm = opt_re.match(cline) if cm: letter = cm.group(1) opt_text = undouble(cm.group(2).strip()).lower() # Find which question on this page has this option text for line in all_lines: nm2 = q_num_re.match(line) if nm2: qn = int(nm2.group(1)) if qn in questions: q = questions[qn] if letter in q['opts']: stored = q['opts'][letter].lower() # fuzzy match: check if colored text is substring or vice versa col_clean = opt_text[:30] sto_clean = stored[:30] if col_clean == sto_clean or col_clean in stored or stored in opt_text: if q['ans'] is None: # don't overwrite q['ans'] = letter break # Also do a direct match: for each colored line, find any question with matching option for page_data_idx in range(0): # skip - already done above pass # Better: do a global pass matching colored option text -> question # Build reverse index: option_text_prefix -> (q_num, letter) opt_index = {} # text_prefix -> list of (q_num, letter) for qn, q in questions.items(): for letter, text in q['opts'].items(): key = text.lower()[:40] if key not in opt_index: opt_index[key] = [] opt_index[key].append((qn, letter)) # Now re-process all colored lines globally with pdfplumber.open(pdf_path) as pdf: # First collect all colored lines per page with context of what questions are on that page page_q_nums = {} # page_i -> set of question nums for page_i, page in enumerate(pdf.pages): chars = page.chars if not chars: continue all_lines = chars_to_lines(chars) nums = set() for line in all_lines: nm = q_num_re.match(line) if nm: nums.add(int(nm.group(1))) page_q_nums[page_i] = nums for page_i, page in enumerate(pdf.pages): chars = page.chars if not chars: continue colored_lines = chars_to_lines([c for c in chars if is_answer_color(c.get('non_stroking_color'))]) if not colored_lines: continue # Questions visible on this page + adjacent pages nearby_nums = set() for pi in range(max(0,page_i-1), min(len(page_q_nums), page_i+2)): nearby_nums |= page_q_nums.get(pi, set()) for cline in colored_lines: cm = opt_re.match(cline) if not cm: continue letter = cm.group(1) opt_text = undouble(cm.group(2).strip()) key = opt_text.lower()[:40] # Find matching question candidates = opt_index.get(key, []) if not candidates: # Try shorter prefix key20 = key[:20] candidates = [(qn,l) for k,(qn_list) in opt_index.items() for qn,l in qn_list if k.startswith(key20)] # Prefer candidates from nearby pages nearby_candidates = [(qn,l) for qn,l in candidates if qn in nearby_nums and l==letter] all_candidates = [(qn,l) for qn,l in candidates if l==letter] for qn, l in (nearby_candidates or all_candidates): if qn in questions and questions[qn]['ans'] is None: questions[qn]['ans'] = l break answered = {qn:q for qn,q in questions.items() if q['ans']} print(f"Total questions: {len(questions)}") print(f"Questions with answer: {len(answered)}") # Save q_list = sorted(questions.values(), key=lambda q: q['num']) with open('/tmp/workspace/immunology_questions_v2.json','w') as f: json.dump(q_list, f, indent=2) # Show sample for q in q_list[:10]: print(f"Q{q['num']}: {q['q'][:60]}... ans={q['ans']}") PYEOF
~/extract_answers.py
import pdfplumber, re, json
pdf_path = '/tmp/workspace/attachments/a75578c1-3477-4037-9a6c-bf3890326414/Immunology _260703_164214.pdf'
ANSWER_COLOR_R = 0.78039217
def is_answer_color(color):
if color is None: return False
if isinstance(color,(list,tuple)) and len(color)==3:
return abs(color[0]-ANSWER_COLOR_R)<0.02
return False
def chars_to_lines(char_list):
if not char_list: return []
sorted_chars = sorted(char_list, key=lambda c: (round(c['top'],0), c['x0']))
lines, current_line, current_y = [], [], None
for c in sorted_chars:
y = round(c['top'],0)
if current_y is None or abs(y-current_y)<5:
current_line.append(c); current_y=y
else:
lines.append(''.join(ch['text'] for ch in current_line))
current_line=[c]; current_y=y
if current_line:
lines.append(''.join(ch['text'] for ch in current_line))
return [l.strip() for l in lines if l.strip()]
def undouble(s):
result=[]; i=0
while i<len(s):
if i+1<len(s) and s[i]==s[i+1] and s[i] not in (' ','\n','\t'):
result.append(s[i]); i+=2
else:
result.append(s[i]); i+=1
return ''.join(result)
q_num_re = re.compile(r'^(\d+)[.\)]\s*(.*)', re.DOTALL)
opt_re = re.compile(r'^([A-EE])[.\)] ?(.*)', re.DOTALL)
questions = {} # num -> dict
# Pass 1: collect all question text and options from normal text
with pdfplumber.open(pdf_path) as pdf:
state = {'num': None, 'q_lines': [], 'opts': {}}
def flush_q():
n = state['num']
if n and (state['q_lines'] or state['opts']):
if n not in questions:
questions[n] = {'num':n, 'q':'', 'opts':{}, 'ans':None}
if state['q_lines'] and not questions[n]['q']:
questions[n]['q'] = undouble(' '.join(state['q_lines']).strip())
for k,v in state['opts'].items():
if k not in questions[n]['opts']:
questions[n]['opts'][k] = undouble(v.strip())
state['num'] = None
state['q_lines'] = []
state['opts'] = {}
for page_i, page in enumerate(pdf.pages):
chars = page.chars
if not chars: continue
all_lines = chars_to_lines(chars)
for line in all_lines:
nm = q_num_re.match(line)
om = opt_re.match(line)
if nm:
new_num = int(nm.group(1))
if new_num != state['num']:
flush_q()
state['num'] = new_num
rest = nm.group(2).strip()
state['q_lines'] = [rest] if rest else []
# same num = duplicate from page overlap, skip
elif om and state['num']:
letter = om.group(1)
text = om.group(2).strip()
if letter not in state['opts']:
state['opts'][letter] = text
elif state['num'] and not state['opts']:
state['q_lines'].append(line)
flush_q()
print(f"Pass 1: {len(questions)} questions parsed")
# Pass 2: build opt_index for fast lookup
opt_index = {} # (letter, text_prefix40) -> list of q_nums
for qn, q in questions.items():
for letter, text in q['opts'].items():
key = (letter, text.lower()[:40])
if key not in opt_index:
opt_index[key] = []
opt_index[key].append(qn)
# Pass 2: match colored lines to answers
with pdfplumber.open(pdf_path) as pdf:
# Build page -> question_nums map
page_qnums = {}
for page_i, page in enumerate(pdf.pages):
chars = page.chars
if not chars: continue
lines = chars_to_lines(chars)
nums = set()
for line in lines:
nm = q_num_re.match(line)
if nm:
nums.add(int(nm.group(1)))
page_qnums[page_i] = nums
total_matched = 0
for page_i, page in enumerate(pdf.pages):
chars = page.chars
if not chars: continue
colored_chars = [c for c in chars if is_answer_color(c.get('non_stroking_color'))]
if not colored_chars: continue
colored_lines = chars_to_lines(colored_chars)
# Nearby question numbers
nearby = set()
for pi in range(max(0,page_i-1), min(len(page_qnums), page_i+2)):
nearby |= page_qnums.get(pi, set())
for cline in colored_lines:
cm = opt_re.match(cline)
if not cm: continue
letter = cm.group(1)
opt_text = undouble(cm.group(2).strip())
key = (letter, opt_text.lower()[:40])
candidates = opt_index.get(key, [])
# Try shorter prefix if no match
if not candidates:
short = opt_text.lower()[:20]
candidates = [qn for (l,prefix),qnums in opt_index.items()
if l==letter and prefix.startswith(short)
for qn in qnums]
# Prefer nearby page questions, then any
nearby_c = [qn for qn in candidates if qn in nearby]
chosen = nearby_c or candidates
for qn in chosen:
if qn in questions and questions[qn]['ans'] is None:
questions[qn]['ans'] = letter
total_matched += 1
break
answered = sum(1 for q in questions.values() if q['ans'])
print(f"Pass 2: matched {total_matched} answers, total with answer = {answered}")
q_list = sorted(questions.values(), key=lambda q: q['num'])
# Show sample
for q in q_list[:5]:
print(f"Q{q['num']}: ans={q['ans']} | {q['q'][:50]}")
with open('/tmp/workspace/immunology_questions_v2.json','w') as f:
json.dump(q_list, f, indent=2)
print("Saved to immunology_questions_v2.json")
python3 /tmp/workspace/extract_answers.py
python3 << 'PYEOF' import json, html as htmllib with open('/tmp/workspace/immunology_questions_v2.json') as f: questions = json.load(f) # Filter: need at least 2 options valid_qs = [q for q in questions if len(q.get('opts',{})) >= 2] # De-duplicate by number seen = set(); deduped = [] for q in valid_qs: if q['num'] not in seen: seen.add(q['num']); deduped.append(q) print(f"Valid: {len(deduped)}, with answers: {sum(1 for q in deduped if q['ans'])}") def esc(s): return htmllib.escape(str(s)) cards_html = [] for idx, q in enumerate(deduped): opts = q.get('opts', {}) ans = q.get('ans') opts_html = [] for letter in ['A','B','C','D','E']: if letter not in opts: continue is_correct = (letter == ans) dc = ' data-correct="true"' if is_correct else '' opts_html.append( f'<li class="option"{dc} data-letter="{letter}" onclick="chooseOption(this)">' f'<span class="opt-letter">{letter}</span>' f'<span class="opt-text">{esc(opts[letter])}</span></li>' ) opts_str = '\n'.join(opts_html) if ans and ans in opts: reveal_html = (f'<div class="reveal hidden" id="reveal-{idx}">' f'<span class="correct-badge">✓ Correct: {ans}) {esc(opts[ans])}</span></div>') else: reveal_html = (f'<div class="reveal hidden" id="reveal-{idx}">' f'<span class="no-ans-badge">Answer key not available for this question</span></div>') dot = "<span class='has-ans' title='Answer available'>●</span>" if ans else "" cards_html.append(f'''<div class="card" id="card-{idx}" data-num="{q["num"]}"> <div class="q-header"><span class="q-num">Q{q["num"]}</span>{dot}</div> <p class="q-text">{esc(q["q"])}</p> <ul class="options">{opts_str}</ul> {reveal_html} <button class="reveal-btn" onclick="revealAnswer({idx})">Show Answer</button> </div>''') all_cards = '\n'.join(cards_html) total = len(deduped) with_ans = sum(1 for q in deduped if q['ans']) HTML = f'''<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Immunology MCQ Exam – {total} Questions</title> <style> :root{{ --bg:#0f1117;--card-bg:#1a1d27;--border:#2a2d3e; --accent:#7c6af7;--accent2:#5eead4; --correct:#22c55e;--wrong:#ef4444; --text:#e2e8f0;--muted:#94a3b8; }} *{{box-sizing:border-box;margin:0;padding:0}} body{{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);padding-bottom:60px}} header{{background:linear-gradient(135deg,#1e1b4b,#312e81);padding:28px 20px 20px;text-align:center;border-bottom:2px solid var(--accent);position:sticky;top:0;z-index:100}} header h1{{font-size:1.7rem;color:#c7d2fe}} header p{{color:var(--muted);margin-top:5px;font-size:.9rem}} .stats{{display:flex;gap:12px;justify-content:center;margin-top:12px;flex-wrap:wrap}} .stat-pill{{background:rgba(255,255,255,.08);border-radius:20px;padding:5px 14px;font-size:.82rem;color:#a5b4fc}} .controls{{max-width:940px;margin:16px auto 0;padding:0 14px;display:flex;gap:10px;flex-wrap:wrap;align-items:center}} .search-box{{flex:1;min-width:180px;padding:9px 14px;border-radius:8px;border:1px solid var(--border);background:var(--card-bg);color:var(--text);font-size:.9rem}} .search-box::placeholder{{color:var(--muted)}} .fbtn{{padding:8px 15px;border-radius:8px;border:1px solid var(--border);background:var(--card-bg);color:var(--muted);cursor:pointer;font-size:.82rem;transition:all .15s}} .fbtn:hover,.fbtn.active{{background:var(--accent);color:#fff;border-color:var(--accent)}} #prog-wrap{{max-width:940px;margin:10px auto 0;padding:0 14px}} #prog-bar{{background:var(--border);border-radius:8px;height:7px;overflow:hidden}} #prog-fill{{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));width:0%;transition:width .4s;border-radius:8px}} #prog-label{{text-align:right;font-size:.78rem;color:var(--muted);margin-top:3px}} .grid{{max-width:940px;margin:18px auto 0;padding:0 14px;display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:18px}} .card{{background:var(--card-bg);border:1px solid var(--border);border-radius:14px;padding:18px;transition:border-color .2s,transform .1s}} .card:hover{{border-color:#4a4d6e;transform:translateY(-1px)}} .card.correct-card{{border-color:var(--correct)!important}} .card.wrong-card{{border-color:var(--wrong)!important}} .q-header{{display:flex;justify-content:space-between;align-items:center;margin-bottom:9px}} .q-num{{font-size:.78rem;font-weight:700;background:var(--accent);color:#fff;padding:3px 10px;border-radius:20px}} .has-ans{{color:var(--accent2);font-size:.72rem}} .q-text{{font-size:.9rem;line-height:1.6;color:#cbd5e1;margin-bottom:12px}} .options{{list-style:none;display:flex;flex-direction:column;gap:7px;margin-bottom:12px}} .option{{display:flex;align-items:flex-start;gap:9px;padding:9px 12px;border-radius:8px;border:1px solid var(--border);cursor:pointer;transition:all .15s;user-select:none}} .option:hover{{background:#252839;border-color:#5a5d8e}} .option.selected{{background:#2a2d3e;border-color:var(--accent)}} .option.show-correct{{background:rgba(34,197,94,.15);border-color:var(--correct)}} .option.show-wrong{{background:rgba(239,68,68,.12);border-color:var(--wrong)}} .opt-letter{{font-weight:700;font-size:.82rem;color:var(--accent);min-width:16px;flex-shrink:0}} .opt-text{{font-size:.85rem;line-height:1.5;color:#d1d5db}} .reveal{{margin-bottom:10px;padding:9px 12px;border-radius:8px;font-size:.85rem;font-weight:600}} .reveal.hidden{{display:none}} .correct-badge{{color:var(--correct);background:rgba(34,197,94,.1);padding:7px 12px;border-radius:8px;display:block;border-left:3px solid var(--correct)}} .no-ans-badge{{color:var(--muted);background:rgba(148,163,184,.07);padding:7px 12px;border-radius:8px;display:block;border-left:3px solid var(--muted)}} .reveal-btn{{width:100%;padding:8px;border-radius:8px;border:1px solid var(--accent);background:transparent;color:var(--accent);font-size:.85rem;cursor:pointer;transition:all .15s;font-weight:600}} .reveal-btn:hover{{background:var(--accent);color:#fff}} .reveal-btn.revealed{{background:rgba(124,106,247,.12);color:#a78bfa;border-color:#a78bfa;cursor:default}} #no-results{{text-align:center;padding:60px;color:var(--muted);display:none;grid-column:1/-1}} .toast{{position:fixed;bottom:22px;right:22px;background:var(--card-bg);border:1px solid var(--accent);border-radius:12px;padding:12px 18px;font-size:.88rem;color:var(--text);box-shadow:0 8px 32px rgba(0,0,0,.4);z-index:200;opacity:0;transform:translateY(16px);transition:all .3s;pointer-events:none}} .toast.show{{opacity:1;transform:translateY(0)}} </style> </head> <body> <header> <h1>🧬 Immunology MCQ Exam</h1> <p>Select an option then click <strong>Show Answer</strong> to reveal the correct answer</p> <div class="stats"> <span class="stat-pill">📋 {total} Questions</span> <span class="stat-pill">✅ {with_ans} With Answer Key</span> <span class="stat-pill" id="s-answered">🎯 0 Answered</span> <span class="stat-pill" id="s-correct">✓ 0 Correct</span> <span class="stat-pill" id="s-score">Score: —</span> </div> </header> <div class="controls"> <input type="text" class="search-box" id="search" placeholder="🔍 Search questions..." oninput="applyFilters()"> <button class="fbtn active" onclick="setFilter('all',this)">All</button> <button class="fbtn" onclick="setFilter('answered',this)">Answered</button> <button class="fbtn" onclick="setFilter('unanswered',this)">Unanswered</button> <button class="fbtn" onclick="setFilter('has-key',this)">Has Key ●</button> <button class="fbtn" id="btn-reveal-all" onclick="revealAll()">Reveal All</button> </div> <div id="prog-wrap"> <div id="prog-bar"><div id="prog-fill"></div></div> <div id="prog-label">0 / {total} answered</div> </div> <div class="grid" id="grid"> {all_cards} <div id="no-results">No questions match your search.</div> </div> <div class="toast" id="toast"></div> <script> const TOTAL={total}; let answered=0,correct=0,currentFilter='all',allRevealed=false; function chooseOption(el){{ const card=el.closest('.card'); if(card.dataset.locked)return; card.querySelectorAll('.option').forEach(o=>o.classList.remove('selected')); el.classList.add('selected'); card.dataset.selected=el.dataset.letter; }} function revealAnswer(idx){{ const card=document.getElementById('card-'+idx); const rev=document.getElementById('reveal-'+idx); const btn=card.querySelector('.reveal-btn'); if(card.dataset.locked)return; card.dataset.locked='1'; btn.classList.add('revealed'); btn.textContent='Answer Shown'; rev.classList.remove('hidden'); const corrEl=card.querySelector('[data-correct]'); const corrLetter=corrEl?corrEl.dataset.letter:null; const selLetter=card.dataset.selected; if(corrLetter){{ card.querySelectorAll('.option').forEach(o=>{{ if(o.dataset.letter===corrLetter)o.classList.add('show-correct'); else if(o.dataset.letter===selLetter&&selLetter!==corrLetter)o.classList.add('show-wrong'); }}); }} if(!card.dataset.counted){{ card.dataset.counted='1'; answered++; if(corrLetter){{ if(selLetter===corrLetter){{correct++;card.classList.add('correct-card');showToast('✓ Correct!','#22c55e');}} else{{card.classList.add('wrong-card');if(selLetter)showToast('✗ Wrong – Correct: '+corrLetter,'#ef4444');}} }} updateStats(); }} applyFilters(); }} function updateStats(){{ document.getElementById('s-answered').textContent='🎯 '+answered+' Answered'; document.getElementById('s-correct').textContent='✓ '+correct+' Correct'; const scored=document.querySelectorAll('.card[data-counted][data-locked]'); const withKey=[...scored].filter(c=>c.querySelector('[data-correct]')).length; document.getElementById('s-score').textContent=withKey?'Score: '+correct+'/'+withKey+' ('+Math.round(correct/withKey*100)+'%)' :'Score: —'; document.getElementById('prog-fill').style.width=(answered/TOTAL*100)+'%'; document.getElementById('prog-label').textContent=answered+' / '+TOTAL+' answered'; }} let toastTimer; function showToast(msg,color){{ const t=document.getElementById('toast'); t.textContent=msg;t.style.borderColor=color; t.classList.add('show');clearTimeout(toastTimer); toastTimer=setTimeout(()=>t.classList.remove('show'),1800); }} function setFilter(f,btn){{ currentFilter=f; document.querySelectorAll('.fbtn').forEach(b=>b.classList.remove('active')); btn.classList.add('active');applyFilters(); }} function applyFilters(){{ const q=document.getElementById('search').value.toLowerCase(); let vis=0; document.querySelectorAll('.card').forEach(card=>{{ const txt=card.querySelector('.q-text').textContent.toLowerCase(); const matchQ=!q||txt.includes(q); let matchF=true; if(currentFilter==='answered')matchF=!!card.dataset.locked; if(currentFilter==='unanswered')matchF=!card.dataset.locked; if(currentFilter==='has-key')matchF=!!card.querySelector('[data-correct]'); card.style.display=(matchQ&&matchF)?'':'none'; if(matchQ&&matchF)vis++; }}); document.getElementById('no-results').style.display=vis===0?'block':'none'; }} function revealAll(){{ if(allRevealed)return;allRevealed=true; document.getElementById('btn-reveal-all').textContent='All Revealed'; document.querySelectorAll('.card').forEach((card,idx)=>{{ if(!card.dataset.locked)revealAnswer(idx); }}); }} </script> </body> </html>''' out = '/tmp/workspace/immunology_exam_v2.html' with open(out,'w',encoding='utf-8') as f: f.write(HTML) import os print(f"Written: {out} ({os.path.getsize(out)//1024} KB)") print(f"Total Q: {total}, With answers: {with_ans}") PYEOF
immunology exam v2
HTML File · HTML
| Before | Now | |
|---|---|---|
| Total questions | 684 | 1,001 |
| With correct answer | 103 | 469 |
autoimmunity and tolarence i need every thing from abbas book\

| Location | Where | Mechanism |
|---|---|---|
| Central tolerance | Thymus (T cells), Bone marrow (B cells) | Deletion, receptor editing (B cells), Treg development |
| Peripheral tolerance | Secondary lymphoid organs and tissues | Anergy, deletion, suppression by Tregs |
| Factor | Favors Tolerance | Favors Immunity |
|---|---|---|
| Antigen dose | High or very low doses | Intermediate dose |
| Adjuvants | Absent | Present |
| Costimulation | Absent | B7 expression on APCs |
| Route | Oral, mucous membrane | Intradermal, subcutaneous |
| Antigen form | Soluble, monomeric | Aggregated, particulate |
Challenge: Little success so far in inducing tolerance once autoimmunity is established. Detecting antigen-specific lymphocytes to measure tolerance induction remains technically difficult.
| Disease | HLA Association |
|---|---|
| Rheumatoid arthritis | HLA-DR4, DR1 |
| Type 1 diabetes | HLA-DR3, DR4 (risk); HLA-DR2 (protective) |
| Ankylosing spondylitis | HLA-B27 |
| SLE | HLA-DR2, DR3 |
| Multiple sclerosis | HLA-DR2 (DRB1*15:01) |
| Gene | Function | Disease Association |
|---|---|---|
| PTPN22 | Protein tyrosine phosphatase (signaling regulation) | RA, type 1 diabetes, Hashimoto's |
| NOD2 | Cytoplasmic bacterial peptidoglycan sensor | Crohn's disease (IBD) |
| IL-23R | Receptor for IL-23 (Th17 differentiation) | IBD, psoriasis |
| CD25 (IL-2Rα) | IL-2 receptor α-chain; Treg generation/function | MS, type 1 diabetes |
| FcγRIIB | Inhibitory Fc receptor on B cells | SLE |
| ATG16L1 | Autophagy protein | Crohn's disease |
| Insulin gene | Affects thymic insulin expression (AIRE-mediated) | Type 1 diabetes |
| CTLA4 | Inhibitory receptor on T cells | RA, type 1 diabetes, Graves' |
| Gene | Protein | Disease/Syndrome |
|---|---|---|
| AIRE | AIRE (thymic antigen expression) | APS-1/APECED (polyendocrinopathy) |
| FOXP3 | FOXP3 transcription factor (Tregs) | IPEX syndrome |
| IL2 | IL-2 cytokine | Autoimmunity + immunodeficiency |
| IL2R (CD25) | IL-2 receptor α-chain (Tregs) | Autoimmunity + immunodeficiency |
| CTLA4 | CTLA-4 inhibitory receptor | Systemic inflammation, lymphoproliferation |
| FAS | Fas (CD95) - apoptosis receptor | ALPS (lymphoproliferation + autoimmunity) |
| FASL | Fas ligand | ALPS |
| IL10 | IL-10 (suppressive cytokine) | Colitis |
| IL10R | IL-10 receptor | Early-onset colitis |
Paradoxically, infections may sometimes prevent autoimmunity - the "hygiene hypothesis" proposes that lack of childhood infections in developed countries reduces regulatory immune responses and increases autoimmune/allergic disease risk.
| Concept | Key Points |
|---|---|
| Tolerance definition | Antigen-specific unresponsiveness induced by antigen exposure |
| Central tolerance - T cells | Negative selection (deletion) in thymus; AIRE enables ectopic antigen expression |
| Central tolerance - B cells | Receptor editing (primary), deletion; anergy for weak self-reactors |
| Peripheral tolerance - T cells | Anergy (no costimulation), CTLA-4/PD-1 inhibitory receptors, Treg suppression, Fas-mediated deletion |
| Peripheral tolerance - B cells | Dependent on T cell tolerance (no T cell help); FcγRIIB inhibition |
| Tregs | CD4+FOXP3+; suppress via CTLA-4, IL-10, TGF-β, IL-2 consumption; FOXP3 mutations → IPEX |
| CTLA-4 | Competes with CD28 for B7; inhibits T activation; haploinsufficiency → autoimmunity |
| PD-1/PD-L1 | Inhibits activated T cells; checkpoint blockade (anti-PD-1) → anti-tumor but causes autoimmunity |
| Autoimmunity causes | Failure of central/peripheral tolerance + genetic susceptibility + infection triggers |
| Genetics | HLA strongest (DR4/RA; DR3,DR4/T1D; B27/AS); PTPN22, NOD2, IL-23R, FcγRIIB also important |
| Infections trigger autoimmunity | Molecular mimicry, bystander activation, epitope spreading, costimulator upregulation |
| Key Mendelian diseases | IPEX (FOXP3), ALPS (Fas/FasL), APECED (AIRE), IPEX-like (CTLA4 haploinsufficiency) |
pd1
| Ligand | Also Called | Expression |
|---|---|---|
| PD-L1 | B7-H1 (CD274) | APCs + many non-lymphoid tissue cells (widespread) |
| PD-L2 | B7-DC (CD273) | Mainly bone marrow-derived APCs only |

In short: PD-1 acts as a signaling inhibitor - it directly dephosphorylates and shuts down TCR and CD28 activation cascades simultaneously.
| Feature | CTLA-4 | PD-1 |
|---|---|---|
| Major site of action | Secondary lymphoid organs (lymph nodes, spleen) | Peripheral tissues |
| Stage inhibited | Induction/priming phase | Effector phase |
| Cell type mainly inhibited | CD4+ ≥ CD8+ | CD8+ > CD4+ |
| Main cellular expression | Tregs + activated T cells | Activated T cells |
| Mechanism | Competitive inhibitor of CD28 (sequesters B7 molecules from APCs) | Signaling inhibitor - recruits SHP2 to block TCR + CD28 kinase signals |
| Principal physiologic role | Preventing autoimmune responses to self antigens | Preventing excessive/pathologic responses to microbes (esp. viruses) + some self antigens |
| Knockout mouse phenotype | Severe multi-organ autoimmunity, early death (3-4 wks) | Milder autoimmunity, organ-specific (lupus-like or cardiomyopathy) |
Abbas: "PD-1 appears to be most important for terminating responses of effector T cells, especially CD8+ cells, in peripheral tissues, whereas CTLA-4 limits the initial activation of T cells in secondary lymphoid organs."
| Drug | Target | Examples | Approved Cancers |
|---|---|---|---|
| Anti-PD-1 | Blocks PD-1 on T cells | Nivolumab (Opdivo), Pembrolizumab (Keytruda) | Melanoma, NSCLC, RCC, Hodgkin lymphoma, many others |
| Anti-PD-L1 | Blocks PD-L1 on tumor/APCs | Atezolizumab, Durvalumab, Avelumab | Bladder cancer, NSCLC, Merkel cell carcinoma |
| Feature | Detail |
|---|---|
| Family | CD28 superfamily (inhibitory member) |
| Expressed on | Antigen-activated T cells (CD4+ and CD8+), B cells, NK cells |
| Ligands | PD-L1 (broad tissue expression) + PD-L2 (APCs only) |
| Signaling | ITIM + ITSM phosphorylated → recruits SHP2 → dephosphorylates TCR + CD28 signals |
| Effect | T cell functional inactivation (not apoptosis) |
| Site of action | Peripheral tissues (effector phase) |
| Physiologic role | Peripheral tolerance, limit viral immunopathology, prevent T cell exhaustion damage |
| Knockout phenotype | Mild organ-specific autoimmunity (milder than CTLA-4 KO) |
| Therapeutic target | Anti-PD-1/PD-L1 checkpoint blockade for cancers |
| Side effect of blockade | Immune-related adverse events (autoimmunity in multiple organs) |