convitom commited on
Commit
3671ffb
·
1 Parent(s): d4a6e08
Files changed (5) hide show
  1. demo/__init__.py +0 -0
  2. demo/app.py +204 -0
  3. demo/cxr_demo.py +261 -0
  4. model/a.py +4 -0
  5. scripts/cxrvlm_demo.ipynb +154 -0
demo/__init__.py ADDED
File without changes
demo/app.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py
3
+ ------
4
+ Gradio chatbot demo for the trained CXR-VLM.
5
+
6
+ Flow
7
+ ----
8
+ 1. Upload a chest X-ray image.
9
+ 2. The model auto-generates a report — Findings + Impression (cascade,
10
+ matching report_mode=split_cascade) — as the assistant's first message.
11
+ 3. Ask any follow-up question about the same image; it's answered VQA-style,
12
+ the way the model was trained.
13
+
14
+ The image stays "loaded" for the whole conversation; uploading a new image
15
+ resets the chat and produces a fresh report.
16
+
17
+ Usage (Colab / cloud GPU — auto-download the checkpoint from the HF runs repo)
18
+ -----------------------------------------------------------------------------
19
+ python -m demo.app \
20
+ --hf_repo hieu3636/cxr-vlm-runs \
21
+ --run_id MIMIC-CXR_resized_run_3 \
22
+ --share # public Gradio link (needed on Colab)
23
+
24
+ Usage (local — checkpoint already on disk)
25
+ ------------------------------------------
26
+ python -m demo.app \
27
+ --checkpoint checkpoints/IU-Xray_run_1/stage2_instruct # dir OR .pt
28
+
29
+ Notes
30
+ -----
31
+ * On Colab pass --share so you get a public URL (the in-VM port isn't
32
+ reachable otherwise).
33
+ * --hf_token is optional if the runs repo is public or $HF_TOKEN is set.
34
+ """
35
+
36
+ import argparse
37
+ import sys
38
+ from pathlib import Path
39
+
40
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
41
+
42
+ from demo.cxr_demo import CXRChatbot, download_checkpoint_from_hf
43
+
44
+
45
+ WELCOME = (
46
+ "Upload a chest X-ray to begin. CXR-HIEU writes a radiology report for it, "
47
+ "then you can ask any follow-up question about the same image."
48
+ )
49
+
50
+
51
+ def parse_args():
52
+ p = argparse.ArgumentParser(description="CXR-VLM chatbot demo (Gradio)")
53
+ # Checkpoint source — either a local path OR an HF runs-repo coordinate.
54
+ p.add_argument("--checkpoint", type=str, default=None,
55
+ help="Local checkpoint dir (contains checkpoint_projection.pt "
56
+ "+ checkpoint_lora/) or a stage2_final.pt path. "
57
+ "If omitted, --hf_repo + --run_id are used to download.")
58
+ p.add_argument("--hf_repo", type=str, default="hieu3636/cxr-vlm-runs",
59
+ help="HF runs repo to pull the checkpoint from.")
60
+ p.add_argument("--run_id", type=str, default="MIMIC-CXR_resized_run_3",
61
+ help="Run id on the HF runs repo, e.g. MIMIC-CXR_resized_run_3.")
62
+ p.add_argument("--stage", type=str, default="stage2",
63
+ choices=["stage2", "stage1"],
64
+ help="Which stage's checkpoint to load (default stage2).")
65
+ p.add_argument("--which", type=str, default="best",
66
+ choices=["best", "last"],
67
+ help="best (lowest eval_loss) or last (default best).")
68
+ p.add_argument("--hf_token", type=str, default=None,
69
+ help="HF token (defaults to $HF_TOKEN). Needed for private repos.")
70
+ p.add_argument("--model_config", type=str, default="configs/model_config.yaml")
71
+ p.add_argument("--device", type=str, default="auto",
72
+ choices=["auto", "cuda", "cpu"])
73
+ p.add_argument("--max_new_tokens", type=int, default=300)
74
+ p.add_argument("--share", action="store_true",
75
+ help="Create a public Gradio link (use this on Colab).")
76
+ p.add_argument("--server_name", type=str, default="0.0.0.0")
77
+ p.add_argument("--server_port", type=int, default=7860)
78
+ return p.parse_args()
79
+
80
+
81
+ def resolve_checkpoint(args) -> str:
82
+ if args.checkpoint:
83
+ return args.checkpoint
84
+ if not args.run_id:
85
+ raise SystemExit(
86
+ "Provide --checkpoint <path>, OR --run_id <id> (with --hf_repo) "
87
+ "to auto-download from the HF runs repo."
88
+ )
89
+ print(f"[app] downloading {args.run_id}/{args.stage}/{args.which} "
90
+ f"from {args.hf_repo} …")
91
+ return download_checkpoint_from_hf(
92
+ repo_id = args.hf_repo,
93
+ run_id = args.run_id,
94
+ stage = args.stage,
95
+ which = args.which,
96
+ token = args.hf_token,
97
+ )
98
+
99
+
100
+ def build_ui(bot: CXRChatbot, max_new_tokens: int):
101
+ import gradio as gr
102
+
103
+ with gr.Blocks(title="CXR-HIEU Demo", theme=gr.themes.Soft()) as demo:
104
+ gr.Markdown(
105
+ "# 🩻 CXR-HIEU — Chest X-ray Report & Q&A\n"
106
+ "Upload a chest X-ray. CXR-HIEU generates a radiology report and "
107
+ "answers questions about the image."
108
+ )
109
+
110
+ # Holds the active conversation's session dict {img, pnu}. The image
111
+ # (and its predicted PNU) persist across follow-up questions; a new
112
+ # upload replaces it → the bot answers about the new image.
113
+ sess_state = gr.State(value=None)
114
+
115
+ with gr.Row():
116
+ with gr.Column(scale=1):
117
+ image_in = gr.Image(type="pil", label="Chest X-ray image",
118
+ height=380)
119
+ analyze_btn = gr.Button("🔍 Analyze image", variant="primary")
120
+ gr.Markdown(
121
+ "_Upload an image and click **Analyze image** (it also "
122
+ "analyzes automatically on upload). Then ask questions on "
123
+ "the right._"
124
+ )
125
+ with gr.Column(scale=2):
126
+ chatbot = gr.Chatbot(label="Conversation", height=460,
127
+ type="messages")
128
+ with gr.Row():
129
+ msg = gr.Textbox(
130
+ placeholder="Ask about this image, e.g. Is there pleural effusion?",
131
+ scale=5, show_label=False, container=False,
132
+ )
133
+ send_btn = gr.Button("Send", scale=1, variant="primary")
134
+ clear_btn = gr.Button("🗑️ Clear conversation")
135
+
136
+ # ── handlers ───────────────────────────────────────────────────────
137
+
138
+ def on_analyze(image):
139
+ """New image → new session, reset chat, generate the report."""
140
+ if image is None:
141
+ return None, [{
142
+ "role": "assistant",
143
+ "content": "⚠️ Please upload a chest X-ray first.",
144
+ }]
145
+ session = bot.prepare(image)
146
+ report = bot.generate_report(session, max_new_tokens=max_new_tokens)
147
+ history = [{"role": "assistant", "content": report}]
148
+ return session, history
149
+
150
+ def on_send(message, history, session):
151
+ history = history or []
152
+ if not message or not message.strip():
153
+ return "", history
154
+ if session is None:
155
+ history.append({"role": "user", "content": message})
156
+ history.append({
157
+ "role": "assistant",
158
+ "content": "⚠️ Please upload an image and click "
159
+ "**Analyze image** before asking questions.",
160
+ })
161
+ return "", history
162
+ answer = bot.answer_question(session, message.strip(),
163
+ max_new_tokens=max_new_tokens)
164
+ history.append({"role": "user", "content": message})
165
+ history.append({"role": "assistant", "content": answer})
166
+ return "", history
167
+
168
+ def on_clear():
169
+ return None, None, ""
170
+
171
+ # Auto-analyze on upload AND via the button.
172
+ image_in.upload(on_analyze, inputs=image_in,
173
+ outputs=[sess_state, chatbot])
174
+ analyze_btn.click(on_analyze, inputs=image_in,
175
+ outputs=[sess_state, chatbot])
176
+ send_btn.click(on_send, inputs=[msg, chatbot, sess_state],
177
+ outputs=[msg, chatbot])
178
+ msg.submit(on_send, inputs=[msg, chatbot, sess_state],
179
+ outputs=[msg, chatbot])
180
+ clear_btn.click(on_clear, outputs=[sess_state, chatbot, msg])
181
+
182
+ gr.Markdown(f"\n\n_{WELCOME}_")
183
+
184
+ return demo
185
+
186
+
187
+ def main():
188
+ args = parse_args()
189
+ ckpt = resolve_checkpoint(args)
190
+ bot = CXRChatbot(
191
+ checkpoint_dir = ckpt,
192
+ model_config = args.model_config,
193
+ device = args.device,
194
+ )
195
+ demo = build_ui(bot, args.max_new_tokens)
196
+ demo.queue().launch(
197
+ share = args.share,
198
+ server_name = args.server_name,
199
+ server_port = args.server_port,
200
+ )
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()
demo/cxr_demo.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ cxr_demo.py
3
+ -----------
4
+ Reusable inference core for the CXR-VLM chatbot demo.
5
+
6
+ Wraps the trained model into the operations the UI needs:
7
+
8
+ 1. download_checkpoint_from_hf(...) — pull {run_id}/stage2/best/ from the HF
9
+ runs repo into a local dir that `load_checkpoint` understands.
10
+ 2. CXRChatbot(...) — loads the model once, then:
11
+ .prepare(PIL.Image) → session dict {img, pnu} kept in UI state
12
+ .generate_report(session) → cascade findings → impression
13
+ .answer_question(session, q) → VQA-style free-form answer
14
+
15
+ PNU (CheXpert classifier) support
16
+ ---------------------------------
17
+ Runs trained on MIMIC-CXR with the Stage-0 classifier ship a
18
+ `checkpoint_chexpert_classifier.pt` inside `stage2/best/`. When that file is
19
+ present we ENABLE the classifier and, per image, predict the PNU 3-section
20
+ string ("Positive/Negative/Uncertain Abnormalities: ...") and prepend it to the
21
+ findings + VQA prompts — exactly the abnormality-guidance the model saw in
22
+ training. IU-Xray runs have no such file → PNU is disabled automatically.
23
+
24
+ Cascade (report_mode=split_cascade) mirrors training:
25
+ • findings — prompt context = PNU (or none if no classifier)
26
+ • impression — prompt context = "Findings: <generated findings>" (NOT PNU)
27
+ • vqa — prompt context = PNU (or none)
28
+
29
+ So at inference we generate findings first, then feed them back as the
30
+ impression's context. No ground truth needed.
31
+
32
+ The model is single-turn: each answer is conditioned on (image + current
33
+ prompt) only — it does NOT carry prior chat turns as text. The UI keeps the
34
+ image (and its PNU) in session state so follow-up questions stay about the same
35
+ image, and a new upload starts a fresh session.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import os
41
+ import sys
42
+ from pathlib import Path
43
+ from typing import Optional, Dict, Any
44
+
45
+ # Make the repo root importable when this file is run directly.
46
+ _REPO_ROOT = Path(__file__).resolve().parents[1]
47
+ if str(_REPO_ROOT) not in sys.path:
48
+ sys.path.insert(0, str(_REPO_ROOT))
49
+
50
+ # Silence per-shard HF download tqdm spam before transformers is imported.
51
+ import utils._quiet # noqa: F401,E402
52
+
53
+ CLASSIFIER_FILE = "checkpoint_chexpert_classifier.pt"
54
+
55
+
56
+ # ──────────────────────────────────────────────────────────────────────────
57
+ # Checkpoint download
58
+ # ──────────────────────────────────────────────────────────────────────────
59
+
60
+ def download_checkpoint_from_hf(
61
+ repo_id: str,
62
+ run_id: str,
63
+ stage: str = "stage2",
64
+ which: str = "best",
65
+ token: Optional[str] = None,
66
+ local_root: str = "checkpoints/_demo_ckpt",
67
+ ) -> str:
68
+ """
69
+ Download `{run_id}/{stage}/{which}/` from the HF *runs* repo and return the
70
+ local directory holding `checkpoint_projection.pt`, `checkpoint_lora/`, and
71
+ (for MIMIC runs) `checkpoint_chexpert_classifier.pt`.
72
+
73
+ The returned path is passed straight to `utils.checkpoint.load_checkpoint`,
74
+ which (given a directory) loads files prefixed with `checkpoint_` — the
75
+ exact names the training callbacks upload to `{stage}/best/`.
76
+
77
+ Args:
78
+ repo_id: e.g. "hieu3636/cxr-vlm-runs"
79
+ run_id: e.g. "MIMIC-CXR_resized_run_3"
80
+ stage: "stage2" (instruction-tuned, default) or "stage1"
81
+ which: "best" (lowest eval_loss, default) or "last"
82
+ token: HF token; falls back to $HF_TOKEN.
83
+ local_root: where to mirror the repo subtree locally.
84
+
85
+ Returns:
86
+ Absolute path to the local `{which}/` directory.
87
+ """
88
+ from huggingface_hub import snapshot_download
89
+
90
+ token = token or os.environ.get("HF_TOKEN")
91
+ subpath = f"{run_id}/{stage}/{which}"
92
+ staging = Path(local_root)
93
+ staging.mkdir(parents=True, exist_ok=True)
94
+
95
+ snapshot_download(
96
+ repo_id = repo_id,
97
+ repo_type = "model",
98
+ token = token,
99
+ allow_patterns = [f"{subpath}/**"],
100
+ local_dir = str(staging),
101
+ )
102
+
103
+ ckpt_dir = staging / run_id / stage / which
104
+ if not ckpt_dir.is_dir():
105
+ raise FileNotFoundError(
106
+ f"Expected {subpath}/ in {repo_id} but nothing was downloaded to "
107
+ f"{ckpt_dir}. Check repo_id / run_id / stage / which."
108
+ )
109
+ proj = ckpt_dir / "checkpoint_projection.pt"
110
+ if not proj.is_file():
111
+ found = "\n ".join(p.name for p in ckpt_dir.iterdir())
112
+ raise FileNotFoundError(
113
+ f"{proj} not found. Files present in {ckpt_dir}:\n {found}\n"
114
+ f"(The loader needs checkpoint_projection.pt + checkpoint_lora/.)"
115
+ )
116
+ return str(ckpt_dir)
117
+
118
+
119
+ # ──────────────────────────────���───────────────────────────────────────────
120
+ # Chatbot core
121
+ # ──────────────────────────────────────────────────────────────────────────
122
+
123
+ class CXRChatbot:
124
+ """Loads the trained CXR-VLM once and serves report + VQA generation."""
125
+
126
+ def __init__(
127
+ self,
128
+ checkpoint_dir: str,
129
+ model_config: str = "configs/model_config.yaml",
130
+ device: str = "auto",
131
+ cpu_dtype: str = "float32",
132
+ use_pnu: str = "auto", # "auto" | True | False
133
+ ):
134
+ import torch
135
+ from omegaconf import OmegaConf
136
+ from model import CXRVisionLanguageModel
137
+ from model.rad_dino import BioViLTEncoder
138
+ from utils.checkpoint import load_checkpoint
139
+
140
+ self.torch = torch
141
+ self.device = self._resolve_device(device)
142
+
143
+ # Enable the CheXpert (PNU) classifier iff the checkpoint ships one,
144
+ # unless the caller forces it on/off.
145
+ clf_file = Path(checkpoint_dir) / CLASSIFIER_FILE
146
+ if use_pnu == "auto":
147
+ self.use_pnu = clf_file.is_file()
148
+ else:
149
+ self.use_pnu = bool(use_pnu)
150
+ if self.use_pnu and not clf_file.is_file():
151
+ print(f"[CXRChatbot] WARNING: use_pnu forced on but {clf_file} "
152
+ "is missing — PNU prediction will use random weights.")
153
+
154
+ model_cfg = OmegaConf.load(model_config)
155
+ self._patch_cfg_for_device(model_cfg, self.device, cpu_dtype)
156
+ model_cfg.chexpert_classifier.enabled = self.use_pnu
157
+
158
+ print(f"[CXRChatbot] building model on device={self.device} "
159
+ f"(PNU/CheXpert guidance: {'ON' if self.use_pnu else 'OFF'}) …")
160
+ self.model = CXRVisionLanguageModel(model_cfg)
161
+ load_checkpoint(self.model, checkpoint_dir)
162
+
163
+ # device_map already placed the (quantized) LLM; move the rest.
164
+ self.model = self.model.to(self.device)
165
+ self.model.eval()
166
+
167
+ self.transform = BioViLTEncoder.get_transform("val")
168
+ print("[CXRChatbot] ready.")
169
+
170
+ # ── device helpers (mirror evaluation/inference.py) ────────────────────
171
+
172
+ def _resolve_device(self, choice: str) -> str:
173
+ import torch
174
+ if choice == "auto":
175
+ return "cuda" if torch.cuda.is_available() else "cpu"
176
+ if choice == "cuda" and not torch.cuda.is_available():
177
+ raise SystemExit("device=cuda requested but no CUDA GPU is visible.")
178
+ return choice
179
+
180
+ def _patch_cfg_for_device(self, model_cfg, device: str, cpu_dtype: str):
181
+ if device == "cpu":
182
+ model_cfg.llm.load_in_4bit = False
183
+ model_cfg.llm.load_in_8bit = False
184
+ model_cfg.llm.torch_dtype = cpu_dtype
185
+ model_cfg.llm.device_map = None
186
+ print("[CXRChatbot] CPU mode: 4-bit/8-bit disabled "
187
+ f"(dtype={cpu_dtype}, expect very slow generation).")
188
+
189
+ # ── session setup ───────────────────────────────────────────────────────
190
+
191
+ def prepare(self, image) -> Dict[str, Any]:
192
+ """
193
+ PIL.Image → session dict for the conversation:
194
+ {"img": (1,C,H,W) tensor on device, "pnu": str|None}
195
+ PNU is predicted once here and reused for every follow-up question, so
196
+ the (expensive) encoder pass that drives it runs a single time per image.
197
+ """
198
+ img = image.convert("RGB")
199
+ img_t = self.transform(img).unsqueeze(0).to(self.device)
200
+ pnu = None
201
+ if self.use_pnu:
202
+ try:
203
+ pnu = self.model.predict_structured_findings(img_t)[0]
204
+ except Exception as e:
205
+ print(f"[CXRChatbot] PNU prediction failed ({type(e).__name__}: "
206
+ f"{e}); continuing without abnormality guidance.")
207
+ return {"img": img_t, "pnu": pnu}
208
+
209
+ # ── generation ─────────────────────────────────────────────────────────
210
+
211
+ def _generate(self, img_t, prompt: str, max_new_tokens: int) -> str:
212
+ out = self.model.generate(
213
+ images = img_t,
214
+ prompts = [prompt],
215
+ max_new_tokens = max_new_tokens,
216
+ temperature = 1e-5, # greedy; HF complains if exactly 0
217
+ do_sample = False,
218
+ num_beams = 1,
219
+ repetition_penalty = 1.2, # break the greedy template loop
220
+ no_repeat_ngram_size = 3,
221
+ )[0]
222
+ return out.strip()
223
+
224
+ def generate_findings(self, session, max_new_tokens: int = 300) -> str:
225
+ from data.prompt_templates import build_findings_prompt
226
+ prompt = build_findings_prompt(
227
+ structured_findings=session.get("pnu"), randomize=False)
228
+ return self._generate(session["img"], prompt, max_new_tokens)
229
+
230
+ def generate_impression(self, session, findings: str,
231
+ max_new_tokens: int = 200) -> str:
232
+ """Impression conditioned on the (just-generated) findings text."""
233
+ from data.prompt_templates import build_impression_prompt
234
+ ctx = f"Findings: {findings.strip()}" if findings.strip() else None
235
+ prompt = build_impression_prompt(structured_findings=ctx, randomize=False)
236
+ return self._generate(session["img"], prompt, max_new_tokens)
237
+
238
+ def generate_report(self, session, max_new_tokens: int = 300) -> str:
239
+ """
240
+ Cascade: findings → impression, returned as ONE flowing paragraph —
241
+ the findings sentences first, the impression sentences appended at the
242
+ end, with no "Findings:" / "Impression:" labels. Matches the
243
+ split_cascade training recipe (impression sees findings; findings/VQA
244
+ see the PNU abnormality guidance when available).
245
+ """
246
+ findings = self.generate_findings(session, max_new_tokens).strip()
247
+ impression = self.generate_impression(session, findings).strip()
248
+
249
+ report = findings
250
+ if impression:
251
+ if report and not report.endswith((".", "!", "?")):
252
+ report += "."
253
+ report = (report + " " + impression).strip()
254
+ return report if report else "(no output)"
255
+
256
+ def answer_question(self, session, question: str,
257
+ max_new_tokens: int = 200) -> str:
258
+ """Free-form / VQA answer about the image (as the model was trained)."""
259
+ from data.prompt_templates import build_vqa_prompt
260
+ prompt = build_vqa_prompt(question, structured_findings=session.get("pnu"))
261
+ return self._generate(session["img"], prompt, max_new_tokens)
model/a.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from rad_dino import RadDino
2
+ from rad_dino.utils import download_sample_image
3
+ model = RadDino.from_pretrained("rad-dino/rad-dino-base")
4
+ print(model)
scripts/cxrvlm_demo.ipynb ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "bc39a82e",
6
+ "metadata": {},
7
+ "source": [
8
+ "# CXR-HIEU — Demo Chatbot (Colab)\n",
9
+ "\n",
10
+ "Upload a chest X-ray and CXR-HIEU writes a radiology report, then answers follow-up questions (VQA) about the image.\n",
11
+ "\n",
12
+ "**Before running:** `Runtime → Change runtime type → GPU` (T4/L4/A100 all work).\n",
13
+ "\n",
14
+ "Just set `RUN_ID` (and `HF_TOKEN` if the runs repo is private) in the config cell, then `Runtime → Run all`. Finally click the `*.gradio.live` link."
15
+ ]
16
+ },
17
+ {
18
+ "cell_type": "markdown",
19
+ "id": "98eae020",
20
+ "metadata": {},
21
+ "source": [
22
+ "## 1. Configuration"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "execution_count": null,
28
+ "id": "21bc5b03",
29
+ "metadata": {},
30
+ "outputs": [],
31
+ "source": [
32
+ "import os\n",
33
+ "\n",
34
+ "# ── REQUIRED: id of the trained run on the HF runs repo ──\n",
35
+ "RUN_ID = \"MIMIC-CXR_resized_run_3\" # run that ships the full PNU CheXpert classifier\n",
36
+ "HF_RUNS = \"hieu3636/cxr-vlm-runs\" # repo holding the run checkpoints\n",
37
+ "HF_CODE = \"hieu3636/cxr-vlm-code\" # repo holding the source code\n",
38
+ "STAGE = \"stage2\" # stage2 = instruction-tuned (default)\n",
39
+ "WHICH = \"best\" # best (lowest eval_loss) | last\n",
40
+ "\n",
41
+ "# ── HF token: needed if the runs repo is private. Prefer Colab Secrets. ──\n",
42
+ "try:\n",
43
+ " from google.colab import userdata\n",
44
+ " os.environ[\"HF_TOKEN\"] = userdata.get(\"HF_TOKEN\")\n",
45
+ "except Exception:\n",
46
+ " os.environ.setdefault(\"HF_TOKEN\", \"\") # hoặc dán token trực tiếp vào đây\n",
47
+ "\n",
48
+ "print(\"RUN_ID =\", RUN_ID)\n",
49
+ "print(\"HF_TOKEN set:\", bool(os.environ.get(\"HF_TOKEN\")))"
50
+ ]
51
+ },
52
+ {
53
+ "cell_type": "markdown",
54
+ "id": "4b30211d",
55
+ "metadata": {},
56
+ "source": [
57
+ "## 2. Pull source code from HF\n",
58
+ "Download the code repo snapshot to `/content/cxr-vlm-code` and `cd` into it."
59
+ ]
60
+ },
61
+ {
62
+ "cell_type": "code",
63
+ "execution_count": null,
64
+ "id": "0a94bc32",
65
+ "metadata": {},
66
+ "outputs": [],
67
+ "source": [
68
+ "from huggingface_hub import snapshot_download\n",
69
+ "import os\n",
70
+ "\n",
71
+ "CODE_DIR = snapshot_download(\n",
72
+ " repo_id=HF_CODE, repo_type='model',\n",
73
+ " token=os.environ.get('HF_TOKEN') or None,\n",
74
+ " local_dir='/content/cxr-vlm-code',\n",
75
+ ")\n",
76
+ "os.chdir(CODE_DIR)\n",
77
+ "print('code dir:', CODE_DIR)\n",
78
+ "!ls"
79
+ ]
80
+ },
81
+ {
82
+ "cell_type": "markdown",
83
+ "id": "6807e7d1",
84
+ "metadata": {},
85
+ "source": [
86
+ "## 3. Install dependencies\n",
87
+ "Install the pinned versions from `requirements.txt` (including `gradio`). PEFT/hub are pinned to match `transformers==4.49`. May take a few minutes."
88
+ ]
89
+ },
90
+ {
91
+ "cell_type": "code",
92
+ "execution_count": null,
93
+ "id": "ff256b17",
94
+ "metadata": {},
95
+ "outputs": [],
96
+ "source": [
97
+ "!pip install -q gradio==5.9.1\n",
98
+ "!pip install -q transformers==4.49.0 peft==0.14.0 accelerate==1.13.0 \\\n",
99
+ " bitsandbytes==0.49.2 'huggingface_hub>=0.27,<1.0' httpx==0.28.1 \\\n",
100
+ " omegaconf==2.3.0 sentencepiece==0.2.1 timm==1.0.26\n",
101
+ "print('done')"
102
+ ]
103
+ },
104
+ {
105
+ "cell_type": "markdown",
106
+ "id": "3baf9e4d",
107
+ "metadata": {},
108
+ "source": [
109
+ "## 4. Load model & launch the demo\n",
110
+ "The first run downloads RAD-DINO + Vicuna-7B (4-bit) + your checkpoint — a few minutes. When ready, open the **`https://....gradio.live`** link printed below."
111
+ ]
112
+ },
113
+ {
114
+ "cell_type": "code",
115
+ "execution_count": null,
116
+ "id": "a6f60d45",
117
+ "metadata": {},
118
+ "outputs": [],
119
+ "source": [
120
+ "import utils._httpx_compat # noqa: F401 (httpx 0.28 compat, safe)\n",
121
+ "from demo.cxr_demo import CXRChatbot, download_checkpoint_from_hf\n",
122
+ "from demo.app import build_ui\n",
123
+ "\n",
124
+ "ckpt_dir = download_checkpoint_from_hf(\n",
125
+ " repo_id=HF_RUNS, run_id=RUN_ID, stage=STAGE, which=WHICH,\n",
126
+ " token=os.environ.get('HF_TOKEN') or None,\n",
127
+ ")\n",
128
+ "print('checkpoint:', ckpt_dir)\n",
129
+ "\n",
130
+ "bot = CXRChatbot(checkpoint_dir=ckpt_dir,\n",
131
+ " model_config='configs/model_config.yaml',\n",
132
+ " device='auto')\n",
133
+ "\n",
134
+ "demo = build_ui(bot, max_new_tokens=300)\n",
135
+ "demo.queue().launch(share=True) # share=True → link công khai *.gradio.live"
136
+ ]
137
+ }
138
+ ],
139
+ "metadata": {
140
+ "accelerator": "GPU",
141
+ "colab": {
142
+ "provenance": []
143
+ },
144
+ "kernelspec": {
145
+ "display_name": "Python 3",
146
+ "name": "python3"
147
+ },
148
+ "language_info": {
149
+ "name": "python"
150
+ }
151
+ },
152
+ "nbformat": 4,
153
+ "nbformat_minor": 5
154
+ }