from __future__ import annotations import os import re import tempfile import threading import time from html import escape import gradio as gr try: import spaces except ImportError: # Local tests should not require the HF Spaces runtime package. class _SpacesFallback: @staticmethod def GPU(*args, **kwargs): def decorator(fn): return fn return decorator spaces = _SpacesFallback() from study_engine import ( DEFAULT_MODEL_ID, DEMO_CASES, EXAMPLE_INPUT, VISION_MODEL_ID, answer_drills, build_rescue_plan, classify_gpu_failure, coach_state, ensure_weights, extract_topics_from_image, free_resident_vlm, load_resident_vlm, packet_to_markdown, time_blocks, ) CSS = """ :root { --ink: #071613; --muted: #1c342f; --muted-soft: #27423c; --paper: #f4e2c5; --card: #fffaf0; --card-solid: #fff8ea; --field: #fffef9; --line: #5e5545; --green: #005844; --green-dark: #032f28; --coral: #84231b; --gold: #755004; --blue: #073e58; --graph: rgba(7, 62, 88, 0.11); --shadow: rgba(37, 29, 16, 0.20); } .gradio-container { background: radial-gradient(circle at 8% 8%, rgba(183, 67, 54, 0.18), transparent 26%), radial-gradient(circle at 92% 4%, rgba(0, 108, 91, 0.18), transparent 24%), linear-gradient(var(--graph) 1px, transparent 1px), linear-gradient(90deg, var(--graph) 1px, transparent 1px), var(--paper); background-size: auto, 24px 24px, 24px 24px, auto; color: var(--ink); font-family: "Trebuchet MS", "Segoe UI", ui-sans-serif, system-ui, sans-serif; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; min-height: 100vh; } .gradio-container, .gradio-container * { text-shadow: none !important; } .gradio-container button:focus-visible, .gradio-container textarea:focus-visible, .gradio-container input:focus-visible, .gradio-container select:focus-visible { outline: 3px solid rgba(0, 108, 91, 0.34) !important; outline-offset: 2px !important; } .app-shell { max-width: 1240px; margin: 0 auto; padding: 24px clamp(14px, 3vw, 34px) 38px; } .hero { position: relative; overflow: hidden; display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; border: 1px solid rgba(7, 22, 19, 0.34); border-radius: 24px; background: linear-gradient(135deg, #fffaf0, #f4d9aa); box-shadow: 0 18px 48px rgba(37, 29, 16, 0.18); padding: clamp(18px, 3vw, 30px); } .hero:after { content: ""; position: absolute; right: -92px; top: -102px; width: 260px; height: 260px; border-radius: 999px; border: 38px solid rgba(183, 67, 54, 0.12); } .eyebrow { display: inline-flex; align-items: center; width: fit-content; border: 1px solid rgba(0, 108, 91, 0.28); border-radius: 999px; background: rgba(0, 88, 68, 0.16); color: var(--green-dark); font-size: 14px; font-weight: 900; letter-spacing: 0.10em; padding: 8px 12px; text-transform: uppercase; } .hero h1 { position: relative; margin: 14px 0 8px; font-family: Georgia, "Times New Roman", ui-serif, serif; color: var(--ink); font-size: clamp(34px, 5vw, 58px); line-height: 0.98; letter-spacing: -0.045em; max-width: 860px; } .hero p { margin: 0; max-width: 720px; color: var(--muted); font-size: clamp(17px, 2vw, 20px); font-weight: 750; line-height: 1.55; } .hero-steps { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; } .hero-steps span { border: 1px solid rgba(7, 22, 19, 0.32); border-radius: 999px; background: #fffdf7; color: var(--ink); font-size: 15px; font-weight: 900; padding: 8px 11px; } .hero-proof { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 18px; max-width: 880px; } .hero-proof div { border: 1px solid rgba(7, 22, 19, 0.30); border-radius: 18px; background: #fffdf7; padding: 12px; } .hero-proof b { display: block; color: var(--coral); font-family: Georgia, "Times New Roman", ui-serif, serif; font-size: clamp(22px, 3vw, 30px); letter-spacing: -0.04em; line-height: 0.95; } .hero-proof span { display: block; margin-top: 5px; color: var(--ink); font-size: 15px; font-weight: 850; line-height: 1.42; } .demo-status { display: grid; grid-template-columns: 1.25fr 1fr 1fr; gap: 10px; margin-top: 16px; } .status-card { border: 1px solid rgba(7, 22, 19, 0.32); border-radius: 20px; background: #fff8ea; box-shadow: 0 16px 40px rgba(37, 29, 16, 0.13); padding: 13px 14px; } .status-card b { display: block; color: var(--green-dark); font-size: 14px; letter-spacing: 0.10em; text-transform: uppercase; } .status-card span { display: block; margin-top: 5px; color: var(--muted); font-size: 15px; font-weight: 750; line-height: 1.45; } .model-budget { display: grid; grid-template-columns: 1.2fr repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 10px; } .budget-card { border: 1px solid rgba(7, 22, 19, 0.34); border-radius: 20px; background: var(--card-solid); padding: 13px 14px; } .budget-card:first-child { background: radial-gradient(circle at top right, rgba(0, 108, 91, 0.16), transparent 46%), var(--card-solid); } .budget-card b { display: block; color: var(--ink); font-size: 14px; font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase; } .budget-card span { display: block; margin-top: 6px; color: var(--muted); font-size: 15px; font-weight: 750; line-height: 1.45; } #main-workspace { gap: 18px; margin-top: 20px; align-items: flex-start; } .input-card, .output-stack { border: 1px solid rgba(7, 22, 19, 0.34); border-radius: 26px; background: var(--card); box-shadow: 0 18px 52px rgba(37, 29, 16, 0.16); padding: clamp(14px, 2vw, 20px); } @media (min-width: 941px) { .input-card { position: sticky; top: 16px; } } .section-title { margin-bottom: 14px; } .section-title h2 { margin: 0; font-family: Georgia, "Times New Roman", ui-serif, serif; color: var(--ink); font-size: 26px; letter-spacing: -0.02em; } .section-title p { margin: 6px 0 0; color: var(--muted); font-size: 16px; font-weight: 750; line-height: 1.5; } .panel { border: 1px solid rgba(7, 22, 19, 0.30); border-radius: 20px; background: #fffef9; box-shadow: none; margin-bottom: 10px; padding: 13px 15px; } .panel h3 { color: var(--green-dark); font-family: Georgia, "Times New Roman", ui-serif, serif; letter-spacing: -0.01em; } .panel h3:first-child { margin-top: 0; } .panel ul, .final-sheet ul { padding-left: 1.15rem; } .panel li, .final-sheet li { margin-bottom: 5px; } .output-stack pre, .output-stack code { max-width: 100% !important; white-space: pre-wrap !important; word-break: break-word !important; } .output-stack pre { overflow-x: auto !important; } .input-card textarea, .input-card input, .input-card select { border-radius: 14px !important; border-color: rgba(7, 22, 19, 0.48) !important; background: var(--field) !important; color: var(--ink) !important; font-size: 16px !important; font-weight: 750 !important; line-height: 1.45 !important; } .input-card label, .input-card .wrap label { color: var(--ink) !important; font-size: 15px !important; font-weight: 900 !important; } .gradio-container input::placeholder, .gradio-container textarea::placeholder { color: #4f625d !important; opacity: 1 !important; } .gradio-container .prose, .gradio-container .markdown, .gradio-container .prose p, .gradio-container .prose li, .gradio-container .prose span, .gradio-container .markdown p, .gradio-container .markdown li, .gradio-container .markdown span { color: var(--ink) !important; font-size: 16px !important; font-weight: 700; line-height: 1.55; } .gradio-container .prose h1, .gradio-container .prose h2, .gradio-container .prose h3, .gradio-container .markdown h1, .gradio-container .markdown h2, .gradio-container .markdown h3 { color: var(--ink) !important; font-weight: 900 !important; } .gradio-container .block-info, .gradio-container .form .secondary-wrap, .gradio-container label span, .gradio-container .wrap span { color: var(--muted) !important; font-size: 14px !important; font-weight: 700 !important; opacity: 1 !important; } .primary-action button, button.primary-action { background: var(--green) !important; border-color: var(--green) !important; border-radius: 16px !important; color: white !important; font-weight: 850 !important; min-height: 46px; box-shadow: 0 12px 28px rgba(0, 108, 91, 0.24); } .primary-action button:hover, button.primary-action:hover { background: var(--green-dark) !important; } .secondary-action button, button.secondary-action { border-color: var(--coral) !important; color: var(--coral) !important; background: #fff7ed !important; border-radius: 16px !important; font-weight: 800 !important; min-height: 46px; } /* Force the cream/light design even when the visitor's device is in dark mode. */ .gradio-container, .gradio-container.dark, .dark { color-scheme: light; --body-background-fill: var(--paper); --background-fill-primary: var(--field); --background-fill-secondary: var(--card-solid); --block-background-fill: var(--card); --block-label-background-fill: var(--card); --block-title-background-fill: var(--card); --input-background-fill: var(--field); --body-text-color: var(--ink); --body-text-color-subdued: var(--muted); --block-label-text-color: var(--ink); --block-title-text-color: var(--ink); --block-info-text-color: var(--muted); --border-color-primary: rgba(7, 22, 19, 0.34); --border-color-accent: rgba(0, 88, 68, 0.34); } .gradio-container.dark .input-card, .gradio-container.dark .output-stack, .dark .input-card, .dark .output-stack { background: var(--card) !important; } #model-note { margin-top: 10px; border-left: 4px solid var(--gold); border-radius: 12px; background: rgba(189, 143, 34, 0.10); padding: 10px 12px; font-size: 15px; font-weight: 800; color: #241800; } .privacy-note { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 14px; border: 1px solid rgba(0, 88, 68, 0.30); border-left: 4px solid var(--green); border-radius: 16px; background: rgba(0, 108, 91, 0.07); padding: 12px 14px; } .privacy-note .lock { font-size: 18px; line-height: 1.35; } .privacy-note b { display: block; color: var(--green-dark); font-size: 13px; font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 3px; } .privacy-note span { display: block; color: var(--muted); font-size: 14px; font-weight: 750; line-height: 1.5; } .rescue-flow { border: 1px solid rgba(7, 22, 19, 0.30); border-radius: 18px; background: radial-gradient(circle at top right, rgba(0, 108, 91, 0.10), transparent 50%), #fffdf7; padding: 13px 15px; margin-bottom: 14px; } .rescue-flow b { display: block; color: var(--green-dark); font-size: 13px; font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 9px; } .rescue-flow ol { margin: 0; padding: 0; list-style: none; counter-reset: rf; display: grid; gap: 7px; } .rescue-flow li { position: relative; padding-left: 30px; color: var(--ink); font-size: 14.5px; font-weight: 800; line-height: 1.4; } .rescue-flow li:before { counter-increment: rf; content: counter(rf); position: absolute; left: 0; top: -1px; width: 21px; height: 21px; border-radius: 999px; background: var(--green); color: #fff; font-size: 12px; font-weight: 900; text-align: center; line-height: 21px; } .coach-hint { margin: 8px 0 2px; color: var(--muted); font-size: 14px; font-weight: 750; line-height: 1.45; } #gen-status .status-busy { border-left: 4px solid var(--gold); border-radius: 12px; background: rgba(189, 143, 34, 0.10); padding: 10px 12px; margin: 8px 0 2px; } @media (prefers-reduced-motion: no-preference) { .status-busy { animation: epr-pulse 1.6s ease-in-out infinite; } @keyframes epr-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } } } .wiz-progress { list-style: none; margin: 0 0 14px; padding: 0; display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; } .wiz-dot { display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center; } .wiz-bub { width: 26px; height: 26px; border-radius: 999px; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 900; border: 1px solid rgba(7, 22, 19, 0.30); background: #fffdf7; color: var(--muted); } .wiz-lab { font-size: 10.5px; font-weight: 850; line-height: 1.2; letter-spacing: 0.02em; color: var(--muted); } .wiz-active .wiz-bub { background: var(--green); border-color: var(--green); color: #fff; box-shadow: 0 6px 16px rgba(0, 108, 91, 0.28); } .wiz-active .wiz-lab { color: var(--green-dark); } .wiz-done .wiz-bub { background: rgba(0, 108, 91, 0.16); border-color: rgba(0, 88, 68, 0.40); color: var(--green-dark); } .wiz-step { border: 1px solid rgba(7, 22, 19, 0.30); border-radius: 18px; background: radial-gradient(circle at top right, rgba(0, 108, 91, 0.08), transparent 50%), #fffdf7; padding: 14px 15px; margin-bottom: 8px; } .wiz-head { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 12px; } .wiz-head .wiz-num { flex: none; width: 24px; height: 24px; border-radius: 999px; background: var(--green); color: #fff; font-size: 13px; font-weight: 900; text-align: center; line-height: 24px; } .wiz-head b { display: block; font-family: Georgia, "Times New Roman", ui-serif, serif; color: var(--ink); font-size: 19px; letter-spacing: -0.01em; } .wiz-head span { display: block; margin-top: 2px; color: var(--muted); font-size: 14px; font-weight: 750; line-height: 1.4; } .wiz-nav { gap: 10px; margin: 6px 0 2px; } .hero-compact { padding: clamp(14px, 2.2vw, 22px); } .hero-compact h1 { font-size: clamp(28px, 4vw, 44px); margin: 10px 0 6px; } .hero-compact p { font-size: clamp(15px, 1.8vw, 18px); } @media (prefers-reduced-motion: no-preference) { .wiz-step { animation: wiz-step-in 280ms ease-out; } @keyframes wiz-step-in { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: translateY(0); } } .wiz-active .wiz-bub { animation: wiz-dot-pop 320ms ease-out; } @keyframes wiz-dot-pop { 0% { transform: scale(0.7); } 60% { transform: scale(1.14); } 100% { transform: scale(1); } } } @media (max-width: 640px) { .wiz-lab { display: none; } } .runtime-label { margin: 4px 0 -4px; color: var(--green-dark); font-size: 14px; font-weight: 850; letter-spacing: 0.12em; text-transform: uppercase; } .final-sheet { border: 1px solid rgba(7, 22, 19, 0.42); border-radius: 24px; background: radial-gradient(circle at top right, rgba(189, 143, 34, 0.25), transparent 34%), linear-gradient(135deg, rgba(0, 98, 79, 0.13), #fffef9); padding: clamp(16px, 3vw, 24px); color: var(--ink); } .sheet-kicker { color: var(--coral); font-size: 12px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; } .final-sheet h2 { margin: 4px 0 14px; font-family: Georgia, "Times New Roman", ui-serif, serif; font-size: clamp(27px, 4vw, 42px); line-height: 0.98; letter-spacing: -0.045em; } .sheet-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } .sheet-grid h3 { margin: 0 0 8px; color: var(--blue); font-weight: 900; } .sheet-rule { border-left: 4px solid var(--green); margin: 12px 0 0; padding: 12px 14px; border-radius: 12px; background: rgba(0, 108, 91, 0.09); font-weight: 700; } .sheet-action, .sheet-proof, .sheet-warning { margin: 12px 0 0; padding: 12px 14px; border-radius: 12px; background: rgba(31, 85, 116, 0.10); } .sheet-proof { border: 1px solid rgba(31, 85, 116, 0.20); } .sheet-warning { border: 1px solid rgba(183, 67, 54, 0.24); background: rgba(183, 67, 54, 0.10); } .sheet-footer { margin: 10px 0 0; color: var(--muted); font-size: 15px; font-weight: 750; } .demo-cases { margin-top: 14px; border: 1px dashed rgba(7, 22, 19, 0.36); border-radius: 18px; background: #fffaf0; box-shadow: none; padding: 12px; } .demo-cases h2 { margin: 0 0 6px; font-family: Georgia, "Times New Roman", ui-serif, serif; color: var(--ink); font-size: 25px; letter-spacing: -0.02em; } .demo-cases p { margin: 0 0 12px; color: var(--muted); font-size: 15px; font-weight: 750; } .case-list { gap: 8px; } .case-button button { justify-content: flex-start !important; width: 100%; min-height: 44px; border: 1px solid rgba(0, 88, 68, 0.36) !important; border-radius: 15px !important; background: #fffef9 !important; color: var(--ink) !important; font-size: 15px !important; font-weight: 800 !important; text-align: left !important; } .case-button button:hover { border-color: rgba(0, 108, 91, 0.36) !important; background: rgba(0, 108, 91, 0.08) !important; } .claim-strip { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 14px; } .claim-card { border: 1px solid rgba(7, 22, 19, 0.30); border-radius: 16px; background: #fffef9; padding: 12px; box-shadow: none; } .claim-card b { display: block; color: var(--green-dark); font-size: 14px; letter-spacing: 0.10em; text-transform: uppercase; } .claim-card span { display: block; margin-top: 6px; color: var(--muted); font-size: 15px; font-weight: 750; line-height: 1.42; } .proof-details { margin-top: 18px; border: 1px solid rgba(7, 22, 19, 0.30); border-radius: 20px; background: #fffaf0; padding: 12px 14px; } .proof-details summary { cursor: pointer; color: var(--green-dark); font-size: 15px; font-weight: 900; } .proof-details p { color: var(--muted); font-size: 15px; font-weight: 750; line-height: 1.5; } .hackathon-footer { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: center; margin-top: 20px; padding: 14px; border: 1px solid rgba(7, 22, 19, 0.22); border-radius: 18px; background: #fffaf0; } .hackathon-footer span { border: 1px solid rgba(0, 88, 68, 0.30); border-radius: 999px; background: #fffef9; color: var(--green-dark); font-size: 13px; font-weight: 850; letter-spacing: 0.04em; padding: 6px 12px; } .runtime-note-tag { display: inline-block; margin: 0 0 6px; color: var(--green-dark); font-size: 13px; font-weight: 850; letter-spacing: 0.06em; text-transform: uppercase; } .coach-card { border: 1px solid rgba(7, 22, 19, 0.30); border-radius: 18px; background: #fffef9; padding: 14px 16px; margin-bottom: 10px; } .coach-idle { color: var(--muted); font-size: 15px; font-weight: 750; line-height: 1.45; } .coach-live { background: radial-gradient(circle at top right, rgba(0, 108, 91, 0.16), transparent 50%), #fffef9; border-color: rgba(0, 88, 68, 0.42); } .coach-now { color: var(--green-dark); font-size: 18px; font-weight: 900; letter-spacing: 0.02em; } .coach-time { font-family: Georgia, "Times New Roman", ui-serif, serif; color: var(--coral); font-size: clamp(34px, 6vw, 46px); line-height: 1.05; letter-spacing: -0.03em; margin: 2px 0; } .coach-next { color: var(--muted); font-size: 14px; font-weight: 800; } .coach-action, .coach-proof { margin-top: 8px; color: var(--ink); font-size: 15px; font-weight: 800; line-height: 1.45; } .coach-proof { border-left: 4px solid var(--gold); border-radius: 10px; background: rgba(189, 143, 34, 0.10); padding: 9px 11px; } .coach-done { background: rgba(0, 108, 91, 0.12); border-color: rgba(0, 88, 68, 0.42); color: var(--green-dark); font-size: 16px; font-weight: 850; } @media (prefers-reduced-motion: no-preference) { .primary-action button, .secondary-action button { transition: transform 150ms ease-out, background-color 150ms ease-out, box-shadow 150ms ease-out; } .primary-action button:hover, .secondary-action button:hover { transform: translateY(-1px); } } @media (max-width: 940px) { .hero { grid-template-columns: 1fr; } .demo-status, .model-budget { grid-template-columns: 1fr; } #main-workspace { flex-direction: column !important; } #main-workspace > .column, #main-workspace > div { width: 100% !important; min-width: 100% !important; } } @media (max-width: 640px) { .app-shell { padding: 12px 10px 24px; } .hero { border-radius: 22px; padding: 18px; } .hero-steps span { width: 100%; } .hero-proof { grid-template-columns: 1fr; } .input-card, .output-stack { border-radius: 20px; padding: 12px; } .sheet-grid { grid-template-columns: 1fr; } .claim-strip { grid-template-columns: 1fr; } .primary-action, .secondary-action { flex: 1 1 100%; } } /* ----- Output tabs: Live Coach / Rescue Plan / Final Sheet / Resources ----- Selectors are deliberately doubled (Gradio class names + ARIA roles) so the cream/green styling lands regardless of the exact Gradio 6 tab DOM. */ #output-tabs { border: none; margin-top: 2px; } #output-tabs > .tab-nav, #output-tabs [role="tablist"] { display: flex; flex-wrap: wrap; gap: 7px; border: none !important; margin-bottom: 12px; } #output-tabs .tab-nav button, #output-tabs button[role="tab"] { border: 1px solid rgba(7, 22, 19, 0.30) !important; border-radius: 14px !important; background: #fffdf7 !important; color: var(--muted) !important; font-size: 14.5px !important; font-weight: 850 !important; padding: 8px 14px !important; min-height: 40px; } #output-tabs .tab-nav button:hover, #output-tabs button[role="tab"]:hover { border-color: rgba(0, 108, 91, 0.40) !important; background: rgba(0, 108, 91, 0.07) !important; } #output-tabs .tab-nav button.selected, #output-tabs button[role="tab"][aria-selected="true"] { background: var(--green) !important; border-color: var(--green) !important; color: #fff !important; box-shadow: 0 8px 18px rgba(0, 108, 91, 0.24); } #output-tabs .tabitem, #output-tabs [role="tabpanel"] { padding-top: 6px; } @media (prefers-reduced-motion: no-preference) { #output-tabs .tabitem, #output-tabs [role="tabpanel"] { animation: epr-tab-in 240ms ease-out; } @keyframes epr-tab-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } } /* ----- Clearer wizard step states (active stands out, done is checked) ----- */ .wiz-active .wiz-bub { box-shadow: 0 0 0 4px rgba(0, 108, 91, 0.18), 0 8px 18px rgba(0, 108, 91, 0.30); } .wiz-active .wiz-lab { color: var(--green-dark); font-weight: 900; } .wiz-done .wiz-bub { background: rgba(0, 108, 91, 0.18); border-color: rgba(0, 88, 68, 0.55); color: var(--green-dark); } .wiz-todo .wiz-bub { opacity: 0.78; } """ HERO_HTML = """
Exam Panic Rescue

When time is low, stop rereading everything.

Five guided steps: tell us the exam, share what you have (or a photo), and get a ranked plan, drills, and a live coach.

""" CLAIM_STATUS_HTML = """
Hackathon build proof and claim status

How to review fast: load a sample scenario only to understand the flow, replace it with real exam details when using the product, build the rescue packet, then check the proof target/final sheet and runtime note.

Claim now Backyard AI main track, OpenBMB MiniCPM on ZeroGPU, OpenAI Codex evidence, and Off-Brand custom UI.
Claim after links Best Demo, Community Choice, and Sharing-style build trace once the public video/social links exist.
Do not claim yet Modal, Nemotron, Tiny Titan, fine-tuning, or Best Agent unless matching evidence exists.
Model budgetMiniCPM-V 4.6 fits the <=32B rule; hardware is the real gate.
ZeroGPU verifiedLive Space smoke generated with MiniCPM on CUDA/ZeroGPU; keep calls focused inside quota.
Default targetOpenBMB MiniCPM stays the submission-aligned model path when hardware can run it.
""" FOOTER_HTML = """ """ @spaces.GPU(duration=120) def _gpu_build_plan( student_name: str, subject: str, time_left_minutes: int, exam_format: str, panic_note: str, known_material: str, confidence: int, model_choice: str = "openbmb/MiniCPM-V-4.6", image_path: str | None = None, ): return build_rescue_plan( student_name, subject, time_left_minutes, exam_format, panic_note, known_material, confidence, model_id=model_choice, image_path=image_path, ) def generate( student_name: str, subject: str, time_left_minutes: int, exam_format: str, panic_note: str, known_material: str, confidence: int, model_choice: str = "openbmb/MiniCPM-V-4.6", image_path: str | None = None, ): # Download weights on CPU first so a cold model never eats the GPU duration budget. ensure_weights(model_choice) rewarm_after = False if not _is_default_vlm(model_choice) and "gguf" not in (model_choice or "").lower(): # Alternate GPU model (Nemotron): make sure the resident 16 GB VLM is not # paged into the same window (16 + 8 GB would crowd the 24 GB ceiling). _clear_vram_for_other_model() rewarm_after = True if "gguf" in (model_choice or "").lower(): # The llama.cpp engine runs on CPU — skip the @spaces.GPU wrapper so it does not hold # (or wait on) the GPU. build_rescue_plan routes the GGUF through llama-cpp-python. plan = build_rescue_plan( student_name, subject, time_left_minutes, exam_format, panic_note, known_material, confidence, model_id=model_choice, image_path=image_path, ) return ( plan.rescue_plan_markdown, plan.drill_markdown, plan.triage_markdown, plan.final_sheet_html, plan.demo_receipt_markdown, plan.model_note, ) try: plan = _gpu_build_plan( student_name, subject, time_left_minutes, exam_format, panic_note, known_material, confidence, model_choice, image_path, ) except Exception as exc: # A ZeroGPU worker timeout/abort is raised here in the main process and is not # catchable inside the GPU call, so fall back to the deterministic packet rather # than surfacing an error to the student — and say WHY, honestly, so the student # knows what would make the model run (sign in / retry / pick the CPU model). plan = build_rescue_plan( student_name, subject, time_left_minutes, exam_format, panic_note, known_material, confidence, force_fallback=True, ) reason = classify_gpu_failure(exc) note = f"{plan.model_note} {reason}".strip() if reason else plan.model_note if rewarm_after: _rewarm_default_vlm() return ( plan.rescue_plan_markdown, plan.drill_markdown, plan.triage_markdown, plan.final_sheet_html, plan.demo_receipt_markdown, note, ) if rewarm_after: _rewarm_default_vlm() return ( plan.rescue_plan_markdown, plan.drill_markdown, plan.triage_markdown, plan.final_sheet_html, plan.demo_receipt_markdown, plan.model_note, ) @spaces.GPU(duration=120) def _gpu_extract_from_photo(image_path): return extract_topics_from_image(image_path) def extract_from_photo(image_path): if not image_path: return gr.update(), "Upload a photo first, or just type your topics." ensure_weights(VISION_MODEL_ID) try: topics, note = _gpu_extract_from_photo(image_path) except Exception: return gr.update(), "Photo reading is busy right now (GPU hiccup). Try again, or just type your topics in." if topics: return topics, note return gr.update(), note def download_packet(rescue, drill, triage, final_sheet_html, receipt): md = packet_to_markdown(rescue, drill, triage, final_sheet_html, receipt) path = os.path.join(tempfile.gettempdir(), "exam-panic-rescue-packet.md") with open(path, "w", encoding="utf-8") as handle: handle.write(md) return path @spaces.GPU(duration=120) def _gpu_show_answers(drill_markdown, subject, model_choice): answers, _note = answer_drills(drill_markdown, subject, model_choice) return answers def show_answers(drill_markdown, subject, model_choice="openbmb/MiniCPM-V-4.6"): ensure_weights(model_choice) rewarm_after = False if not _is_default_vlm(model_choice) and "gguf" not in (model_choice or "").lower(): _clear_vram_for_other_model() rewarm_after = True try: return _gpu_show_answers(drill_markdown, subject, model_choice) except Exception: return "### Worked answers\n\nThe model is busy right now (GPU hiccup). Try again in a moment — or self-check each drill against your notes and mark it right or wrong." finally: if rewarm_after: _rewarm_default_vlm() def load_example(): return ( EXAMPLE_INPUT["student_name"], EXAMPLE_INPUT["subject"], EXAMPLE_INPUT["time_left_minutes"], EXAMPLE_INPUT["exam_format"], EXAMPLE_INPUT["panic_note"], EXAMPLE_INPUT["known_material"], EXAMPLE_INPUT["confidence"], ) def load_case(index: int): case = DEMO_CASES[index] return ( case["student_name"], case["subject"], case["time_left_minutes"], case["exam_format"], case["panic_note"], case["known_material"], case["confidence"], ) def load_biology_case(): return load_case(0) def load_physics_case(): return load_case(1) def load_history_case(): return load_case(2) def load_math_case(): return load_case(3) CASE_LOADERS = [load_biology_case, load_physics_case, load_history_case, load_math_case] WIZARD_LABELS = [ "Tell us the exam", "Share what you have", "Scan your image", "Generate plan", "Live coach", ] def wizard_progress_html(step: int) -> str: """Render the step progress bar plus the CSS that shows only the current step. The show/hide is done with CSS (re-emitted on every step change) instead of Gradio visible= updates: all five step columns stay mounted, which keeps field values warm and swaps steps instantly and reliably. """ current = max(1, min(len(WIZARD_LABELS), int(step or 1))) dots = [] for index, label in enumerate(WIZARD_LABELS, start=1): state = "wiz-done" if index < current else ("wiz-active" if index == current else "wiz-todo") glyph = "✓" if state == "wiz-done" else str(index) aria = ' aria-current="step"' if state == "wiz-active" else "" dots.append( f'
  • {glyph}' f'{label}
  • ' ) hidden = ", ".join(f"#wiz-step-{i}" for i in range(1, len(WIZARD_LABELS) + 1) if i != current) style = ( f"" ) return style + '
      ' + "".join(dots) + "
    " def _boot_warmup() -> None: """Warm the default model before the first student clicks. Downloads the MiniCPM-V 4.6 weights and loads them resident (the documented ZeroGPU main-process pattern). Runs in a daemon thread at startup so the UI is never blocked; on CPU-only or local runs every step is a guarded no-op. This removes the silent 60-90 s first-click penalty that previously followed every Space rebuild. """ try: ensure_weights(DEFAULT_MODEL_ID) load_resident_vlm(DEFAULT_MODEL_ID) except Exception: pass threading.Thread(target=_boot_warmup, name="boot-warmup", daemon=True).start() def _is_default_vlm(model_id: str) -> bool: return "minicpm-v" in (model_id or "").lower() def _clear_vram_for_other_model() -> None: """Evict the resident MiniCPM-V in the MAIN process, before a GPU window opens. ZeroGPU pages main-process CUDA tensors into every @spaces.GPU window, so an eviction that happens inside the worker (engine-level) is too late: the 16 GB VLM still gets paged in alongside the alternate Nemotron pipeline and can starve or stall the call. Freeing here, in the main process, is what actually releases it. """ free_resident_vlm() def _rewarm_default_vlm() -> None: """Re-load the resident MiniCPM-V in the background after another model ran.""" threading.Thread( target=load_resident_vlm, args=(DEFAULT_MODEL_ID,), name="vlm-rewarm", daemon=True ).start() GEN_BUSY_HTML = ( '

    Working… normally ~10–25 s once warm; the first ' 'run right after a restart can take longer while the small model wakes up. ' 'Elapsed: 0s. Your packet appears in the results panel.

    ' ) def _gen_busy(): return GEN_BUSY_HTML def _gen_done(): return "" def _vision_busy(): return "⏳ Reading your photo with MiniCPM-V 4.6 — usually well under a minute when the model is warm." def _answers_busy(): return "### Worked answers\n\n⏳ Writing worked answers with the selected small model…" def _plain_text(value) -> str: text = re.sub(r"<[^>]+>", " ", str(value or "")) return re.sub(r"\s+", " ", text).strip() def _triage_item(triage_markdown, label: str) -> str: pattern = rf"-\s*{re.escape(label)}:\s*(.+)" match = re.search(pattern, str(triage_markdown or ""), re.IGNORECASE) return match.group(1).strip() if match else "" def _first_csv_item(value: str) -> str: return next((item.strip() for item in value.split(",") if item.strip()), "") def _coach_instruction(label: str, triage_markdown, final_sheet_html) -> tuple[str, str]: current = (label or "").lower() proof = _triage_item(triage_markdown, "Proof target") first_topic = _first_csv_item(_triage_item(triage_markdown, "Topics extracted")) first_leak = _first_csv_item(_triage_item(triage_markdown, "Weaknesses to attack")) topic = first_topic or "your first protected mark" leak = first_leak or "one visible weak spot" if "reset" in current: action = f"Write what you remember about {topic}, circle {leak}, then commit to the first drill." stop = proof or "Stop this block once the first target is chosen." elif "core" in current or "recall" in current: action = f"Protect the marks you already know. Rebuild {topic} from memory before checking notes." stop = "Stop when you can explain the first protected topic without reopening every chapter." elif "drill" in current or "practice" in current or "quick test" in current: action = "Answer the drills without notes first. Mark mistakes instead of hiding them." stop = proof or "Stop when one drill is answered and one mistake is repaired." elif "patch" in current or "weak" in current or "second" in current: action = f"Patch {leak}. Repair one mistake, then rerun the same style of question." stop = "Stop when the mistake has a corrected version you can repeat." elif "break" in current: action = "Take the break. Stand up, drink water, and do not open a new topic." stop = "Return when the timer moves to the next block." elif "final" in current: action = "Read only the final sheet. No new chapters, no new videos, no last-minute syllabus expansion." stop = "Stop when the final sheet has been read once calmly." else: action = _plain_text(final_sheet_html)[:180] or "Follow the current block from your triage clock." stop = proof or "Move on when the timer changes blocks." return action, stop def _coach_card(state: dict, triage_markdown, final_sheet_html, panic_until=None) -> str: if panic_until and time.time() < float(panic_until): remaining = max(0, int(round(float(panic_until) - time.time()))) return ( '
    ' '
    Panic reset
    ' f'
    00:{remaining:02d}
    ' '
    Put both feet down, unclench your jaw, breathe out slowly, then name the next tiny action.
    ' '
    When this ends, return to the same block. Do not restart the whole plan.
    ' "
    " ) if state["done"]: return '
    Time is up. Read only your final sheet, then walk in.
    ' rs = state["remaining_s"] nxt = f'Next: {state["next"]}' if state["next"] else "Last block: finish with the final sheet" action, stop = _coach_instruction(state["current"], triage_markdown, final_sheet_html) return ( '
    ' f'
    Now: {escape(str(state["current"]))}
    ' f'
    {rs // 60:02d}:{rs % 60:02d} left
    ' f'
    {escape(nxt)} · block {state["index"] + 1}/{state["count"]}
    ' f'
    Do now: {escape(action)}
    ' f'
    Proof/stop: {escape(stop)}
    ' "
    " ) def _advance_elapsed_to_next_block(minutes, elapsed_seconds: float) -> float: blocks = [(label, mins) for label, mins in time_blocks(int(minutes or 60)) if mins > 0] elapsed_min = max(0.0, elapsed_seconds) / 60.0 acc = 0.0 for _, block_minutes in blocks: block_end = acc + block_minutes if elapsed_min < block_end: return block_end * 60 acc = block_end return sum(block_minutes for _, block_minutes in blocks) * 60 # One global client-side script (installed at load): forces the light theme the design # is built for, runs the elapsed-seconds ticker whenever a busy status is on screen, and # smooth-scrolls the wizard card into view when the active step changes. Implemented as a # MutationObserver so it never interferes with Gradio's own event/update plumbing. APP_JS = """ () => { try { const url = new URL(window.location.href); if (url.searchParams.get('__theme') !== 'light') { url.searchParams.set('__theme', 'light'); window.location.replace(url.toString()); } } catch (e) {} try { let tick = null; let lastStep = ''; const ensureTicker = () => { const els = document.querySelectorAll('.epr-elapsed'); els.forEach((el) => { if (!el.dataset.t0) el.dataset.t0 = String(Date.now()); }); if (els.length && !tick) { tick = setInterval(() => { const live = document.querySelectorAll('.epr-elapsed'); if (!live.length) { clearInterval(tick); tick = null; return; } live.forEach((el) => { el.textContent = Math.round((Date.now() - Number(el.dataset.t0)) / 1000) + 's'; }); }, 1000); } }; const onStepChange = () => { const active = document.querySelector('.wiz-dot.wiz-active .wiz-lab'); const label = active ? active.textContent : ''; if (label && lastStep && label !== lastStep && label !== 'Live coach') { const card = document.querySelector('.input-card'); if (card) card.scrollIntoView({ behavior: 'smooth', block: 'start' }); } if (label) lastStep = label; }; const mo = new MutationObserver(() => { ensureTicker(); onStepChange(); }); mo.observe(document.body, { childList: true, subtree: true }); } catch (e) {} } """ # Build Small Hackathon submission Space (resume). with gr.Blocks(title="Exam Panic Rescue") as demo: gr.HTML(f"", container=False) with gr.Column(elem_classes=["app-shell"]): gr.HTML(HERO_HTML, container=False) with gr.Row(equal_height=False, elem_id="main-workspace"): with gr.Column(scale=5, min_width=320, elem_classes=["input-card"]): gr.HTML( """

    Build your rescue packet

    Five quick steps. The flow moves with you.

    """, container=False, ) wizard_step = gr.State(1) wiz_progress = gr.HTML(wizard_progress_html(1), container=False) with gr.Column(elem_id="wiz-step-1", elem_classes=["wiz-step"]) as step1_col: gr.HTML( """
    1
    Tell us the examName, subject, time left.
    """, container=False, ) student_name = gr.Textbox( label="Student", value=EXAMPLE_INPUT["student_name"], lines=1, info=None, ) subject = gr.Textbox( label="Exam subject", value=EXAMPLE_INPUT["subject"], lines=2, info=None, ) with gr.Row(): exam_format = gr.Dropdown( label="Exam format", choices=["Mixed", "Multiple choice", "Short answer", "Long answer"], value=EXAMPLE_INPUT["exam_format"], info=None, ) confidence = gr.Slider( label="Confidence", minimum=1, maximum=5, value=EXAMPLE_INPUT["confidence"], step=1, info="1 = frozen · 5 = steady.", ) time_left_minutes = gr.Slider( label="Minutes left", minimum=15, maximum=1440, value=EXAMPLE_INPUT["time_left_minutes"], step=15, info="15 min – full day.", ) example = gr.Button("Load example", elem_classes=["secondary-action"]) with gr.Column(elem_id="wiz-step-2", elem_classes=["wiz-step"]) as step2_col: gr.HTML( """
    2
    Share what you havePanic dump, topics — or a photo to scan next.
    """, container=False, ) gr.HTML( """
    Privacy-first note This hosted Space is a public demo. The app does not intentionally store normal user sessions or add them to the public trace dataset. For sensitive use, run the repo locally with the small-model path.
    """, container=False, ) panic_note = gr.Textbox( label="Panic dump", value=EXAMPLE_INPUT["panic_note"], lines=5, info=None, ) known_material = gr.Textbox( label="Syllabus, notes, or weak topics", value=EXAMPLE_INPUT["known_material"], lines=5, info=None, ) with gr.Accordion("📷 Upload syllabus / exam sheet / notes photo", open=True): syllabus_image = gr.Image( label="Syllabus, exam sheet, textbook page, or notes", type="filepath", height=180, ) gr.Markdown( "MiniCPM-V 4.6 reads the image, extracts visible topics, and uses them to build the rescue plan." ) with gr.Column(elem_id="wiz-step-3", elem_classes=["wiz-step"]) as step3_col: gr.HTML( """
    3
    Scan your imageOptional — pull the photo's topics into your inputs.
    """, container=False, ) extract_btn = gr.Button("Extract topics from photo", elem_classes=["secondary-action"]) vision_note = gr.Markdown( "Upload a photo in Step 2, then tap Extract — MiniCPM-V 4.6 reads it and fills your topics box. Always check what it found." ) with gr.Column(elem_id="wiz-step-4", elem_classes=["wiz-step"]) as step4_col: gr.HTML( """
    4
    Generate your rescue planBuild the packet. Topics from your photo are already included.
    """, container=False, ) with gr.Accordion("⚙️ Advanced: choose the small model", open=False): model_choice = gr.Dropdown( label="Generation model (all ≤32B)", choices=[ ("OpenBMB MiniCPM-V 4.6 — default (reads text + your photo)", "openbmb/MiniCPM-V-4.6"), ("NVIDIA Nemotron-Mini-4B (≤4B · Tiny Titan)", "nvidia/Nemotron-Mini-4B-Instruct"), ("OpenBMB MiniCPM4 0.5B · llama.cpp GGUF (CPU runtime · Tiny Titan)", "openbmb/MiniCPM4-0.5B-QAT-Int4-GGUF"), ], value="openbmb/MiniCPM-V-4.6", info="MiniCPM-V 4.6 writes your plan and can read your syllabus photo. Nemotron-4B is a text-only alternate. The 0.5B option runs through the llama.cpp runtime on CPU. The runtime note shows exactly what ran.", ) run = gr.Button("Build my rescue packet", variant="primary", elem_classes=["primary-action"]) gen_status = gr.HTML("", elem_id="gen-status", container=False) with gr.Column(elem_id="wiz-step-5", elem_classes=["wiz-step"]) as step5_col: gr.HTML( """
    5
    Follow with the live coachYour plan is ready — the coach walks you through it.
    """, container=False, ) gr.HTML( '

    After generating your rescue plan, start the live coach to walk through the time blocks step by step. Open the Live Coach tab in the results panel →

    ', container=False, ) with gr.Row(elem_classes=["wiz-nav"]): back_btn = gr.Button("← Back", elem_classes=["secondary-action"], interactive=False) next_btn = gr.Button("Next: Share what you have →", elem_classes=["primary-action"]) inputs = [student_name, subject, time_left_minutes, exam_format, panic_note, known_material, confidence] with gr.Column(elem_classes=["demo-cases"]): gr.HTML( """

    Try a sample scenario

    One click fills every step (demo data, not real users).

    """, container=False, ) case_buttons = [] with gr.Column(elem_classes=["case-list"]): for index, case in enumerate(DEMO_CASES): label = f"{case['name'].title()} · {case['time_left_minutes']} min · {case['exam_format']}" case_buttons.append( ( gr.Button( label, size="lg", elem_classes=["case-button"], ), index, ) ) with gr.Column(scale=7, min_width=340, elem_classes=["output-stack"]): gr.HTML( """

    Your low-time learning packet

    Four tabs: Live Coach, Rescue Plan, Final Sheet, Resources. Your plan opens automatically.

    """, container=False, ) # Invisible state + timer live outside the tabs so coaching keeps running # no matter which tab is open. coach_start = gr.State(None) coach_panic_until = gr.State(None) coach_timer = gr.Timer(1.0, active=False) with gr.Tabs(elem_id="output-tabs") as output_tabs: with gr.Tab("Live coach", id="coach"): gr.HTML('
    Live coach
    ', container=False) coach_display = gr.HTML( value='
    Build a packet, then press Start coaching. The coach gives the next action, proof target, and timer for each block.
    ', container=False, ) with gr.Row(): coach_start_btn = gr.Button("Start coaching", elem_classes=["primary-action"]) coach_done_btn = gr.Button("I finished this block", elem_classes=["secondary-action"]) panic_reset_btn = gr.Button("20-sec panic reset", elem_classes=["secondary-action"]) coach_reset_btn = gr.Button("Reset", elem_classes=["secondary-action"]) gr.HTML( '

    After generating your rescue plan, start the coach. It keeps the current action, proof target, and next block visible while you study.

    ', container=False, ) with gr.Tab("Rescue plan", id="plan"): rescue_output = gr.Markdown( value="### Rescue plan\n\nAppears after you build the packet (step 4).", elem_classes=["panel"], ) drill_output = gr.Markdown( value="### Drill deck\n\nFive drills land here after generation.", elem_classes=["panel"], ) answers_btn = gr.Button("Show worked answers", elem_classes=["secondary-action"]) answers_output = gr.Markdown( value="### Worked answers\n\nReveal model-written answers after you build a packet.", elem_classes=["panel"], ) with gr.Tab("Final sheet", id="sheet"): triage_output = gr.Markdown( value="### Triage clock\n\nYour time blocks land here.", elem_classes=["panel"], ) final_sheet_output = gr.HTML( value='

    Final sheet

    Build a packet to create the one-page sheet to read before the exam.

    ', elem_classes=["panel"], ) download_btn = gr.DownloadButton("⬇ Download / print", elem_classes=["secondary-action"]) with gr.Tab("Resources", id="resources"): demo_receipt_output = gr.Markdown( value="### Study receipt\n\nA short before/after receipt.", elem_classes=["panel"], ) gr.HTML('
    Runtime note
    ', container=False) model_note = gr.Markdown( value="No generation yet. This note always reports exactly which small model ran (default OpenBMB MiniCPM-V 4.6 on ZeroGPU) or why a fallback was used.", elem_id="model-note", ) gr.HTML(CLAIM_STATUS_HTML, container=False) outputs = [ rescue_output, drill_output, triage_output, final_sheet_output, demo_receipt_output, model_note, ] gr.HTML(FOOTER_HTML, container=False) demo.load(js=APP_JS) def _start_coach(minutes, triage_markdown, final_sheet_html): start_ts = time.time() state = coach_state(time_blocks(int(minutes or 60)), 0) return start_ts, None, gr.Timer(active=True), _coach_card(state, triage_markdown, final_sheet_html) def _reset_coach(): return ( None, None, gr.Timer(active=False), '
    Coach reset. Press Start coaching to run your triage clock with action prompts.
    ', ) def _tick_coach(start_ts, panic_until, minutes, triage_markdown, final_sheet_html): if not start_ts: return gr.update() state = coach_state(time_blocks(int(minutes or 60)), time.time() - start_ts) return _coach_card(state, triage_markdown, final_sheet_html, panic_until) def _finish_current_block(start_ts, minutes, triage_markdown, final_sheet_html): if not start_ts: return None, '
    Start the coach first, then you can advance block by block.
    ' next_elapsed = _advance_elapsed_to_next_block(minutes, time.time() - start_ts) new_start = time.time() - next_elapsed state = coach_state(time_blocks(int(minutes or 60)), next_elapsed) return new_start, _coach_card(state, triage_markdown, final_sheet_html) def _panic_reset(start_ts, minutes, triage_markdown, final_sheet_html): if not start_ts: return None, '
    Start the coach first, then use panic reset whenever you freeze.
    ' until = time.time() + 20 state = coach_state(time_blocks(int(minutes or 60)), time.time() - start_ts) return until, _coach_card(state, triage_markdown, final_sheet_html, until) coach_start_btn.click( _start_coach, inputs=[time_left_minutes, triage_output, final_sheet_output], outputs=[coach_start, coach_panic_until, coach_timer, coach_display], ) coach_done_btn.click( _finish_current_block, inputs=[coach_start, time_left_minutes, triage_output, final_sheet_output], outputs=[coach_start, coach_display], ) panic_reset_btn.click( _panic_reset, inputs=[coach_start, time_left_minutes, triage_output, final_sheet_output], outputs=[coach_panic_until, coach_display], ) coach_reset_btn.click(_reset_coach, outputs=[coach_start, coach_panic_until, coach_timer, coach_display]) coach_timer.tick( _tick_coach, inputs=[coach_start, coach_panic_until, time_left_minutes, triage_output, final_sheet_output], outputs=[coach_display], ) def _wizard_go(step): step = max(1, min(len(WIZARD_LABELS), int(step or 1))) back_update = gr.update(interactive=step > 1) if step < len(WIZARD_LABELS): next_update = gr.update(value=f"Next: {WIZARD_LABELS[step]} →", interactive=True) else: next_update = gr.update(value="All steps done ✓", interactive=False) return [step, wizard_progress_html(step), back_update, next_update] def _wizard_next(step): return _wizard_go(int(step or 1) + 1) def _wizard_back(step): return _wizard_go(int(step or 1) - 1) def _wizard_first(): return _wizard_go(1) def _wizard_fifth(): return _wizard_go(5) def _open_plan_tab(): # After generating, surface the fresh packet by opening the Rescue plan tab. # (The left wizard still advances to the Live coach step.) return gr.Tabs(selected="plan") def _advance_after_extract(note_text, step): # Move to step 4 only when the photo actually yielded topics; otherwise stay on # step 3 so the student can read what went wrong (or just type topics instead). if isinstance(note_text, str) and note_text.startswith("Topics read from your photo"): return _wizard_go(4) return _wizard_go(int(step or 3)) nav_outputs = [wizard_step, wiz_progress, back_btn, next_btn] next_btn.click(_wizard_next, inputs=[wizard_step], outputs=nav_outputs) back_btn.click(_wizard_back, inputs=[wizard_step], outputs=nav_outputs) run.click(_gen_busy, outputs=[gen_status], queue=False).then( generate, inputs=inputs + [model_choice, syllabus_image], outputs=outputs, scroll_to_output=True, api_name="generate", ).then(_gen_done, outputs=[gen_status], queue=False).then( _wizard_fifth, outputs=nav_outputs, queue=False, ).then(_open_plan_tab, outputs=[output_tabs], queue=False) extract_btn.click(_vision_busy, outputs=[vision_note], queue=False).then( extract_from_photo, inputs=[syllabus_image], outputs=[known_material, vision_note], api_name="extract_topics", ).then(_advance_after_extract, inputs=[vision_note, wizard_step], outputs=nav_outputs, queue=False) download_btn.click( download_packet, inputs=[rescue_output, drill_output, triage_output, final_sheet_output, demo_receipt_output], outputs=download_btn, ) answers_btn.click(_answers_busy, outputs=[answers_output], queue=False).then( show_answers, inputs=[drill_output, subject, model_choice], outputs=[answers_output], api_name="show_answers", ) example.click(load_example, outputs=inputs, queue=False).then( _wizard_first, outputs=nav_outputs, queue=False, ) for case_button, case_index in case_buttons: case_button.click(CASE_LOADERS[case_index], outputs=inputs, queue=False).then( _wizard_first, outputs=nav_outputs, queue=False, ) if __name__ == "__main__": demo.queue(default_concurrency_limit=1).launch( server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")), show_error=True, )