Barath commited on
Commit
843fc22
Β·
verified Β·
1 Parent(s): ea6d766

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. frontend/index.html +355 -203
frontend/index.html CHANGED
@@ -8,24 +8,15 @@
8
  <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap" rel="stylesheet" />
9
  <style>
10
  :root {
11
- --bg: #04070d;
12
- --panel: #070d18;
13
- --line: #11304a;
14
- --cyan: #4fd6ff;
15
- --cyan-hot: #9ff0ff;
16
- --green: #3dff9e;
17
- --text: #bfe9ff;
18
- --dim: #2f7ea8;
19
  }
20
  * { box-sizing: border-box; margin: 0; padding: 0; }
21
  html, body { height: 100%; }
22
  body {
23
  background: radial-gradient(ellipse at 50% -20%, #0a1c33 0%, var(--bg) 60%) fixed;
24
- color: var(--text);
25
- font-family: 'Share Tech Mono', monospace;
26
- overflow-x: hidden;
27
  }
28
- /* CRT grid + vignette */
29
  body::before {
30
  content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 0;
31
  background-image:
@@ -33,128 +24,85 @@
33
  linear-gradient(90deg, rgba(79,214,255,.04) 1px, transparent 1px);
34
  background-size: 28px 28px;
35
  }
36
- body::after {
37
- content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 1;
38
- background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.55) 100%);
39
- }
40
- #root { position: relative; z-index: 2; max-width: 1180px; margin: 0 auto; padding: 22px 18px 40px; }
41
 
42
  .header {
43
  text-align: center; border: 1px solid var(--line); border-radius: 12px;
44
- padding: 16px 10px 12px;
45
  background: linear-gradient(180deg, #08121f 0%, #060b14 100%);
46
  box-shadow: 0 0 30px rgba(31,167,255,.08), inset 0 0 18px rgba(0,0,0,.6);
47
  }
48
  .header h1 {
49
- letter-spacing: .35em; color: #e6f8ff; font-size: 1.7em; font-weight: 400;
50
  text-shadow: 0 0 14px rgba(79,214,255,.7);
51
  }
52
- .header .sub { color: var(--cyan); letter-spacing: .18em; font-size: .72em; opacity: .85; margin-top: 4px; }
53
- .online { color: var(--green); font-size: .7em; letter-spacing: .25em; }
54
  .online::before { content: '●'; margin-right: .5em; animation: blink 1.6s infinite; }
55
  @keyframes blink { 0%,55% {opacity:1} 60%,100% {opacity:.15} }
56
 
57
- .deck { display: grid; grid-template-columns: 1fr 1.2fr; gap: 18px; margin-top: 18px; }
58
- @media (max-width: 880px) { .deck { grid-template-columns: 1fr; } }
59
-
60
- .panel {
61
- border: 1px solid var(--line); border-radius: 12px; background: var(--panel);
62
- box-shadow: inset 0 0 25px rgba(0,0,0,.7), 0 0 18px rgba(31,167,255,.07);
63
- padding: 14px;
64
- }
65
- .panel .tag {
66
- text-align: center; color: var(--dim); font-size: .68em; letter-spacing: .35em;
67
- margin-bottom: 10px; user-select: none;
68
- }
69
-
70
- /* specimen bay */
71
- .bay {
72
- position: relative; border: 1px dashed #1a4a6e; border-radius: 10px;
73
- min-height: 320px; display: flex; align-items: center; justify-content: center;
74
- cursor: pointer; overflow: hidden; background: #050a12;
75
- transition: border-color .25s;
76
- }
77
- .bay:hover { border-color: var(--cyan); }
78
- .bay img { max-width: 100%; max-height: 420px; display: block; }
79
- .bay .hint { color: var(--dim); text-align: center; font-size: .8em; letter-spacing: .15em; line-height: 2; padding: 30px; }
80
- .bay .hint b { color: var(--cyan); }
81
-
82
- /* gantry: the MRI bore around the radiograph */
83
- .gantry {
84
- position: relative; border-radius: 10px; min-height: 320px; overflow: hidden;
85
- background: #02050a; display: flex; align-items: center; justify-content: center;
86
- border: 1px solid #0c2235;
87
  }
88
- .gantry img.result { max-width: 100%; max-height: 460px; display: block; }
89
- .gantry .idle { color: var(--dim); letter-spacing: .3em; font-size: .75em; }
90
-
91
- /* scan sweep */
92
- .sweep {
93
- position: absolute; left: 0; right: 0; top: -35%; height: 35%; z-index: 6;
94
- background: linear-gradient(180deg, transparent 0%,
95
- rgba(31,167,255,.06) 55%, rgba(79,214,255,.22) 88%, #6fe3ff 99%, #fff 100%);
96
- box-shadow: 0 6px 24px 4px rgba(79,214,255,.55);
97
- animation: sweep 2.2s cubic-bezier(.4,0,.6,1) infinite;
98
- pointer-events: none;
99
- }
100
- @keyframes sweep { 0% {top:-35%} 100% {top:135%} }
101
-
102
- /* rotating bore ring */
103
- .ring {
104
- position: absolute; inset: -8%; z-index: 5; pointer-events: none;
105
- border-radius: 50%;
106
- border: 2px solid rgba(79,214,255,.12);
107
- border-top-color: rgba(159,240,255,.85);
108
- border-bottom-color: rgba(79,214,255,.35);
109
- box-shadow: 0 0 40px rgba(79,214,255,.15), inset 0 0 60px rgba(79,214,255,.05);
110
- animation: spin 1.4s linear infinite;
111
- }
112
- .ring.r2 { inset: -2%; animation-duration: 2.6s; animation-direction: reverse; opacity: .6; }
113
- @keyframes spin { to { transform: rotate(360deg); } }
114
-
115
  .scanning-label {
116
- position: absolute; top: 12px; right: 16px; z-index: 7;
117
  color: #6fe3ff; font-size: 11px; letter-spacing: .4em;
118
  text-shadow: 0 0 10px rgba(79,214,255,.9);
119
  animation: blink .9s infinite; pointer-events: none;
120
  }
 
 
 
 
 
121
 
122
- /* console readout */
123
- .readout {
124
- margin-top: 12px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px;
125
- background: #050b14; min-height: 64px; font-size: .78em; line-height: 1.7;
126
- color: var(--green); white-space: pre-wrap;
127
  }
128
- .readout .dim { color: var(--dim); }
129
 
130
  .btn {
131
- width: 100%; margin-top: 12px; padding: 14px; border-radius: 10px;
132
- font-family: inherit; font-size: 1em; letter-spacing: .3em; cursor: pointer;
133
  color: #aee9ff; background: linear-gradient(180deg, #0e2c47 0%, #0a1d31 100%);
134
  border: 1px solid #1fa7ff; text-shadow: 0 0 10px rgba(79,214,255,.8);
135
  box-shadow: 0 0 22px rgba(31,167,255,.25), inset 0 0 12px rgba(31,167,255,.12);
136
  transition: all .25s;
137
  }
138
  .btn:hover:not(:disabled) { box-shadow: 0 0 36px rgba(31,167,255,.55); color: #fff; }
139
- .btn:disabled { opacity: .45; cursor: not-allowed; }
140
- .btn.secondary { border-color: #15425f; color: #7fc4e4; letter-spacing: .2em; font-size: .8em; padding: 10px; box-shadow: none; }
141
 
142
- .samples { display: flex; gap: 10px; margin-top: 12px; }
143
  .samples .chip {
144
  flex: 1; border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
145
  cursor: pointer; background: #050a12; transition: border-color .2s; position: relative;
146
  }
147
  .samples .chip:hover { border-color: var(--cyan); }
148
- .samples .chip img { width: 100%; height: 84px; object-fit: cover; display: block; opacity: .8; }
149
  .samples .chip span {
150
- position: absolute; bottom: 0; left: 0; right: 0; font-size: .6em; letter-spacing: .25em;
151
- text-align: center; color: var(--cyan); background: rgba(4,9,16,.75); padding: 3px 0;
 
 
 
 
 
 
152
  }
 
153
 
154
- .footer { text-align: center; color: var(--dim); font-size: .65em; letter-spacing: .3em; margin-top: 22px; line-height: 2; }
155
  .footer a { color: var(--cyan); text-decoration: none; }
156
 
157
- /* boot overlay */
158
  #boot {
159
  position: fixed; inset: 0; z-index: 50; background: var(--bg);
160
  display: flex; align-items: center; justify-content: center;
@@ -162,69 +110,47 @@
162
  }
163
  #boot.done { opacity: 0; pointer-events: none; }
164
  #boot pre { color: var(--green); letter-spacing: .15em; line-height: 2; }
 
 
 
 
 
 
 
165
  </style>
166
  </head>
167
  <body>
168
  <div id="boot"><pre id="bootlog"></pre></div>
169
- <div id="root"></div>
170
-
171
- <script type="module">
172
- import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/+esm";
173
-
174
- const $ = (sel) => document.querySelector(sel);
175
-
176
- /* ---------- boot sequence ---------- */
177
- const BOOT_LINES = [
178
- "RADIOLOGY UNIT 07 Β· POWER ON",
179
- "LOADING EMITTER ......... FLUX.2-KLEIN-9B",
180
- "LOADING OPTICS .......... QWEN2.5-VL-3B",
181
- "PARAMETER BUDGET ........ 12B / 32B CAP",
182
- "CALIBRATING GANTRY ...... OK",
183
- "SCANNER ONLINE.",
184
- ];
185
- async function boot() {
186
- const log = $("#bootlog");
187
- for (const line of BOOT_LINES) {
188
- log.textContent += line + "\n";
189
- await new Promise(r => setTimeout(r, 260));
190
- }
191
- await new Promise(r => setTimeout(r, 400));
192
- $("#boot").classList.add("done");
193
- }
194
 
195
- /* ---------- UI skeleton ---------- */
196
- $("#root").innerHTML = `
197
  <div class="header">
198
  <div class="online">SCANNER ONLINE</div>
199
  <h1>X-RAY ANYTHING</h1>
200
  <div class="sub">RADIOLOGY UNIT 07 Β· PHOTOGRAPH ANY OBJECT β€” SEE INSIDE IT</div>
201
  </div>
202
 
203
- <div class="deck">
 
 
 
 
 
 
204
  <div class="panel">
205
- <div class="tag">β–Ό SPECIMEN BAY β–Ό</div>
206
- <div class="bay" id="bay">
207
- <div class="hint" id="bay-hint">
208
- CLICK TO LOAD SPECIMEN<br/>
209
- <b>UPLOAD PHOTO</b> Β· DRAG &amp; DROP<br/>or use an archived specimen below
210
- </div>
211
- <img id="bay-img" style="display:none" alt="specimen"/>
212
- </div>
213
  <input type="file" id="file" accept="image/*" style="display:none"/>
214
- <button class="btn" id="scan-btn" disabled>β–Ά BEGIN SCAN</button>
215
  <div class="samples">
216
  <div class="chip" data-src="/samples/gpu.jpg"><img src="/samples/gpu.jpg" alt=""/><span>SPECIMEN 001 Β· GPU</span></div>
217
  <div class="chip" data-src="/samples/mouse.jpg"><img src="/samples/mouse.jpg" alt=""/><span>SPECIMEN 002 Β· MOUSE</span></div>
218
  </div>
 
219
  </div>
220
-
221
  <div class="panel">
222
- <div class="tag">β–Ό RADIOGRAPH β–Ό</div>
223
- <div class="gantry" id="gantry">
224
- <div class="idle" id="gantry-idle">AWAITING SPECIMEN</div>
225
- <img class="result" id="result-img" style="display:none" alt="radiograph"/>
226
- </div>
227
  <div class="readout" id="readout"><span class="dim">// system idle β€” load a specimen to begin</span></div>
 
228
  <button class="btn secondary" id="dl-btn" style="display:none">⬇ DOWNLOAD RADIOGRAPH</button>
229
  </div>
230
  </div>
@@ -234,32 +160,247 @@ $("#root").innerHTML = `
234
  FLUX.2-KLEIN-9B + QWEN2.5-VL-3B Β· BUILT WITH GRADIO gr.Server Β·
235
  <a href="https://huggingface.co/spaces/build-small-hackathon/xray-anything" target="_blank">SOURCE</a>
236
  </div>
237
- `;
238
 
239
- /* ---------- state & helpers ---------- */
240
- let specimenDataUrl = null;
241
- let scanning = false;
242
- let client = null;
 
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  const readout = $("#readout");
245
  function log(line, color) {
246
- const span = document.createElement("div");
247
- if (color) span.style.color = color;
248
- span.textContent = line;
249
- readout.appendChild(span);
250
  while (readout.childNodes.length > 5) readout.removeChild(readout.firstChild);
251
  }
252
  function clearLog() { readout.innerHTML = ""; }
253
 
254
- function setSpecimen(dataUrl) {
255
- specimenDataUrl = dataUrl;
256
- $("#bay-img").src = dataUrl;
257
- $("#bay-img").style.display = "block";
258
- $("#bay-hint").style.display = "none";
259
- $("#scan-btn").disabled = false;
260
- clearLog();
261
- log("// specimen loaded β€” gantry ready", "#4fd6ff");
 
 
 
 
 
 
 
 
262
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
  async function fileToDataUrl(file) {
265
  return new Promise((res) => {
@@ -269,8 +410,18 @@ async function fileToDataUrl(file) {
269
  });
270
  }
271
 
272
- /* ---------- specimen input ---------- */
273
- $("#bay").addEventListener("click", () => $("#file").click());
 
 
 
 
 
 
 
 
 
 
274
  $("#file").addEventListener("change", async (e) => {
275
  if (e.target.files[0]) setSpecimen(await fileToDataUrl(e.target.files[0]));
276
  });
@@ -286,92 +437,93 @@ document.querySelectorAll(".chip").forEach((chip) => {
286
  });
287
  });
288
 
289
- /* ---------- scanner audio hum (WebAudio, starts on scan) ---------- */
290
  let audioCtx = null, hum = null;
291
  function startHum() {
292
  try {
293
  audioCtx = audioCtx || new AudioContext();
294
- const osc = audioCtx.createOscillator();
295
- const osc2 = audioCtx.createOscillator();
296
  const gain = audioCtx.createGain();
297
  osc.type = "sawtooth"; osc.frequency.value = 48;
298
  osc2.type = "sine"; osc2.frequency.value = 97;
299
- gain.gain.value = 0.025;
300
  osc.connect(gain); osc2.connect(gain); gain.connect(audioCtx.destination);
301
  osc.start(); osc2.start();
302
  hum = { osc, osc2, gain };
303
- } catch { /* no audio, no problem */ }
304
  }
305
  function stopHum() {
306
  if (!hum) return;
307
- hum.gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.4);
308
  setTimeout(() => { hum.osc.stop(); hum.osc2.stop(); hum = null; }, 500);
309
  }
310
 
311
- /* ---------- the scan ---------- */
312
  const STAGES = [
313
- "β—Œ CALIBRATING EMITTER",
314
  "β—Œ IDENTIFYING SPECIMEN",
315
  "β—Œ EXPOSING β€” 4-STEP RECTIFIED FLOW",
316
  "β—Œ RECONSTRUCTING SLICES",
317
  ];
318
 
 
319
  $("#scan-btn").addEventListener("click", async () => {
320
- if (!specimenDataUrl || scanning) return;
321
  scanning = true;
 
322
  $("#scan-btn").disabled = true;
 
323
  $("#dl-btn").style.display = "none";
324
- $("#result-img").style.display = "none";
325
- $("#gantry-idle").style.display = "none";
326
-
327
- const gantry = $("#gantry");
328
- gantry.insertAdjacentHTML("beforeend",
329
- `<div class="sweep" id="fx-sweep"></div>
330
- <div class="ring" id="fx-r1"></div>
331
- <div class="ring r2" id="fx-r2"></div>
332
- <div class="scanning-label" id="fx-label">SCANNING</div>`);
333
- startHum();
334
  clearLog();
335
-
336
- let stage = 0;
337
- const stageTimer = setInterval(() => {
338
  log(STAGES[Math.min(stage, STAGES.length - 1)] + " ...");
339
  stage++;
340
- }, 1800);
341
- log(STAGES[0] + " ...");
342
- stage = 1;
343
 
344
  try {
345
  client = client || await Client.connect(location.origin);
346
  const out = await client.predict("/xray", { image_b64: specimenDataUrl });
347
- const data = out.data[0];
348
- $("#result-img").src = data.image;
349
- $("#result-img").style.display = "block";
350
- clearLog();
351
- log(`// SCAN COMPLETE β€” SPECIMEN: ${String(data.object).toUpperCase()}`, "#3dff9e");
352
- log("// radiograph is an AI reconstruction, not engineering documentation", "#2f7ea8");
353
- $("#dl-btn").style.display = "block";
354
- $("#dl-btn").onclick = () => {
355
- const a = document.createElement("a");
356
- a.href = data.image; a.download = "radiograph.webp"; a.click();
357
- };
358
  } catch (err) {
359
- clearLog();
360
- log("// SCAN FAULT β€” " + (err?.message || err), "#ff6b6b");
361
- log("// check scanner status or retry", "#2f7ea8");
362
- } finally {
363
  clearInterval(stageTimer);
364
  stopHum();
365
- ["fx-sweep", "fx-r1", "fx-r2", "fx-label"].forEach((id) => {
366
- const el = document.getElementById(id);
367
- if (el) el.remove();
368
- });
369
- $("#scan-btn").disabled = false;
370
  scanning = false;
 
371
  }
372
  });
373
 
374
- boot();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  </script>
376
  </body>
377
  </html>
 
8
  <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap" rel="stylesheet" />
9
  <style>
10
  :root {
11
+ --bg: #04070d; --panel: #070d18; --line: #11304a;
12
+ --cyan: #4fd6ff; --green: #3dff9e; --text: #bfe9ff; --dim: #2f7ea8;
 
 
 
 
 
 
13
  }
14
  * { box-sizing: border-box; margin: 0; padding: 0; }
15
  html, body { height: 100%; }
16
  body {
17
  background: radial-gradient(ellipse at 50% -20%, #0a1c33 0%, var(--bg) 60%) fixed;
18
+ color: var(--text); font-family: 'Share Tech Mono', monospace; overflow-x: hidden;
 
 
19
  }
 
20
  body::before {
21
  content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 0;
22
  background-image:
 
24
  linear-gradient(90deg, rgba(79,214,255,.04) 1px, transparent 1px);
25
  background-size: 28px 28px;
26
  }
27
+ #root { position: relative; z-index: 2; max-width: 1180px; margin: 0 auto; padding: 20px 16px 36px; }
 
 
 
 
28
 
29
  .header {
30
  text-align: center; border: 1px solid var(--line); border-radius: 12px;
31
+ padding: 14px 10px 10px;
32
  background: linear-gradient(180deg, #08121f 0%, #060b14 100%);
33
  box-shadow: 0 0 30px rgba(31,167,255,.08), inset 0 0 18px rgba(0,0,0,.6);
34
  }
35
  .header h1 {
36
+ letter-spacing: .35em; color: #e6f8ff; font-size: 1.55em; font-weight: 400;
37
  text-shadow: 0 0 14px rgba(79,214,255,.7);
38
  }
39
+ .header .sub { color: var(--cyan); letter-spacing: .18em; font-size: .7em; opacity: .85; margin-top: 3px; }
40
+ .online { color: var(--green); font-size: .68em; letter-spacing: .25em; }
41
  .online::before { content: '●'; margin-right: .5em; animation: blink 1.6s infinite; }
42
  @keyframes blink { 0%,55% {opacity:1} 60%,100% {opacity:.15} }
43
 
44
+ /* 3D stage */
45
+ #stage-wrap {
46
+ margin-top: 14px; border: 1px solid var(--line); border-radius: 12px;
47
+ background: radial-gradient(ellipse at 50% 30%, #081526 0%, #03060c 75%);
48
+ box-shadow: inset 0 0 40px rgba(0,0,0,.8), 0 0 24px rgba(31,167,255,.07);
49
+ position: relative; overflow: hidden; height: 480px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
+ #stage { width: 100%; height: 100%; display: block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  .scanning-label {
53
+ position: absolute; top: 14px; right: 18px; z-index: 7; display: none;
54
  color: #6fe3ff; font-size: 11px; letter-spacing: .4em;
55
  text-shadow: 0 0 10px rgba(79,214,255,.9);
56
  animation: blink .9s infinite; pointer-events: none;
57
  }
58
+ #stage-hint {
59
+ position: absolute; left: 0; right: 0; bottom: 14px; z-index: 7;
60
+ text-align: center; color: var(--dim); font-size: .68em; letter-spacing: .3em;
61
+ pointer-events: none;
62
+ }
63
 
64
+ .controls { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 14px; }
65
+ @media (max-width: 880px) { .controls { grid-template-columns: 1fr; } }
66
+ .panel {
67
+ border: 1px solid var(--line); border-radius: 12px; background: var(--panel);
68
+ box-shadow: inset 0 0 25px rgba(0,0,0,.7); padding: 12px;
69
  }
70
+ .panel .tag { text-align: center; color: var(--dim); font-size: .64em; letter-spacing: .35em; margin-bottom: 8px; }
71
 
72
  .btn {
73
+ width: 100%; padding: 13px; border-radius: 10px; margin-top: 8px;
74
+ font-family: inherit; font-size: .95em; letter-spacing: .3em; cursor: pointer;
75
  color: #aee9ff; background: linear-gradient(180deg, #0e2c47 0%, #0a1d31 100%);
76
  border: 1px solid #1fa7ff; text-shadow: 0 0 10px rgba(79,214,255,.8);
77
  box-shadow: 0 0 22px rgba(31,167,255,.25), inset 0 0 12px rgba(31,167,255,.12);
78
  transition: all .25s;
79
  }
80
  .btn:hover:not(:disabled) { box-shadow: 0 0 36px rgba(31,167,255,.55); color: #fff; }
81
+ .btn:disabled { opacity: .4; cursor: not-allowed; }
82
+ .btn.secondary { border-color: #15425f; color: #7fc4e4; letter-spacing: .2em; font-size: .78em; padding: 9px; box-shadow: none; margin-top: 8px; }
83
 
84
+ .samples { display: flex; gap: 10px; margin-top: 8px; }
85
  .samples .chip {
86
  flex: 1; border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
87
  cursor: pointer; background: #050a12; transition: border-color .2s; position: relative;
88
  }
89
  .samples .chip:hover { border-color: var(--cyan); }
90
+ .samples .chip img { width: 100%; height: 72px; object-fit: cover; display: block; opacity: .8; }
91
  .samples .chip span {
92
+ position: absolute; bottom: 0; left: 0; right: 0; font-size: .56em; letter-spacing: .22em;
93
+ text-align: center; color: var(--cyan); background: rgba(4,9,16,.78); padding: 3px 0;
94
+ }
95
+
96
+ .readout {
97
+ padding: 9px 11px; border: 1px solid var(--line); border-radius: 8px;
98
+ background: #050b14; min-height: 86px; font-size: .74em; line-height: 1.7;
99
+ color: var(--green); white-space: pre-wrap;
100
  }
101
+ .readout .dim { color: var(--dim); }
102
 
103
+ .footer { text-align: center; color: var(--dim); font-size: .62em; letter-spacing: .3em; margin-top: 18px; line-height: 2; }
104
  .footer a { color: var(--cyan); text-decoration: none; }
105
 
 
106
  #boot {
107
  position: fixed; inset: 0; z-index: 50; background: var(--bg);
108
  display: flex; align-items: center; justify-content: center;
 
110
  }
111
  #boot.done { opacity: 0; pointer-events: none; }
112
  #boot pre { color: var(--green); letter-spacing: .15em; line-height: 2; }
113
+
114
+ /* fullscreen radiograph viewer */
115
+ #viewer {
116
+ position: fixed; inset: 0; z-index: 60; background: rgba(2,5,10,.94);
117
+ display: none; align-items: center; justify-content: center; cursor: zoom-out;
118
+ }
119
+ #viewer img { max-width: 92vw; max-height: 92vh; box-shadow: 0 0 60px rgba(79,214,255,.25); border-radius: 8px; }
120
  </style>
121
  </head>
122
  <body>
123
  <div id="boot"><pre id="bootlog"></pre></div>
124
+ <div id="viewer"><img id="viewer-img" alt="radiograph"/></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ <div id="root">
 
127
  <div class="header">
128
  <div class="online">SCANNER ONLINE</div>
129
  <h1>X-RAY ANYTHING</h1>
130
  <div class="sub">RADIOLOGY UNIT 07 Β· PHOTOGRAPH ANY OBJECT β€” SEE INSIDE IT</div>
131
  </div>
132
 
133
+ <div id="stage-wrap">
134
+ <canvas id="stage"></canvas>
135
+ <div class="scanning-label" id="scan-label">SCANNING</div>
136
+ <div id="stage-hint">LOAD A SPECIMEN β€” IT RIDES THE TABLE INTO THE BORE AND RETURNS AS A RADIOGRAPH</div>
137
+ </div>
138
+
139
+ <div class="controls">
140
  <div class="panel">
141
+ <div class="tag">β–Ό SPECIMEN INTAKE β–Ό</div>
142
+ <button class="btn" id="load-btn">⬆ LOAD SPECIMEN</button>
 
 
 
 
 
 
143
  <input type="file" id="file" accept="image/*" style="display:none"/>
 
144
  <div class="samples">
145
  <div class="chip" data-src="/samples/gpu.jpg"><img src="/samples/gpu.jpg" alt=""/><span>SPECIMEN 001 Β· GPU</span></div>
146
  <div class="chip" data-src="/samples/mouse.jpg"><img src="/samples/mouse.jpg" alt=""/><span>SPECIMEN 002 Β· MOUSE</span></div>
147
  </div>
148
+ <button class="btn" id="scan-btn" disabled>β–Ά BEGIN SCAN</button>
149
  </div>
 
150
  <div class="panel">
151
+ <div class="tag">β–Ό CONSOLE β–Ό</div>
 
 
 
 
152
  <div class="readout" id="readout"><span class="dim">// system idle β€” load a specimen to begin</span></div>
153
+ <button class="btn secondary" id="view-btn" style="display:none">β›Ά VIEW RADIOGRAPH FULLSCREEN</button>
154
  <button class="btn secondary" id="dl-btn" style="display:none">⬇ DOWNLOAD RADIOGRAPH</button>
155
  </div>
156
  </div>
 
160
  FLUX.2-KLEIN-9B + QWEN2.5-VL-3B Β· BUILT WITH GRADIO gr.Server Β·
161
  <a href="https://huggingface.co/spaces/build-small-hackathon/xray-anything" target="_blank">SOURCE</a>
162
  </div>
163
+ </div>
164
 
165
+ <script type="module">
166
+ import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js";
167
+ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/+esm";
168
+
169
+ const $ = (s) => document.querySelector(s);
170
 
171
+ /* ============ boot ============ */
172
+ const BOOT_LINES = [
173
+ "RADIOLOGY UNIT 07 Β· POWER ON",
174
+ "LOADING EMITTER ......... FLUX.2-KLEIN-9B",
175
+ "LOADING OPTICS .......... QWEN2.5-VL-3B",
176
+ "PARAMETER BUDGET ........ 12B / 32B CAP",
177
+ "SPINNING UP GANTRY ...... OK",
178
+ "SCANNER ONLINE.",
179
+ ];
180
+ (async () => {
181
+ const log = $("#bootlog");
182
+ for (const line of BOOT_LINES) {
183
+ log.textContent += line + "\n";
184
+ await new Promise(r => setTimeout(r, 240));
185
+ }
186
+ await new Promise(r => setTimeout(r, 350));
187
+ $("#boot").classList.add("done");
188
+ })();
189
+
190
+ /* ============ console readout ============ */
191
  const readout = $("#readout");
192
  function log(line, color) {
193
+ const div = document.createElement("div");
194
+ if (color) div.style.color = color;
195
+ div.textContent = line;
196
+ readout.appendChild(div);
197
  while (readout.childNodes.length > 5) readout.removeChild(readout.firstChild);
198
  }
199
  function clearLog() { readout.innerHTML = ""; }
200
 
201
+ /* ============ three.js scene ============ */
202
+ const canvas = $("#stage");
203
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
204
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
205
+
206
+ const scene = new THREE.Scene();
207
+ scene.fog = new THREE.Fog(0x03060c, 8, 18);
208
+
209
+ const camera = new THREE.PerspectiveCamera(42, 2, 0.1, 50);
210
+ camera.position.set(0.0, 1.1, 7.2);
211
+
212
+ function resize() {
213
+ const w = canvas.clientWidth, h = canvas.clientHeight;
214
+ renderer.setSize(w, h, false);
215
+ camera.aspect = w / h;
216
+ camera.updateProjectionMatrix();
217
  }
218
+ new ResizeObserver(resize).observe(canvas);
219
+
220
+ scene.add(new THREE.AmbientLight(0x2a4a66, 1.2));
221
+ const key = new THREE.PointLight(0x9fd9ff, 60, 30); key.position.set(4, 5, 6); scene.add(key);
222
+ const rim = new THREE.PointLight(0x1fa7ff, 40, 20); rim.position.set(-5, 2, -2); scene.add(rim);
223
+
224
+ /* --- the machine --- */
225
+ const machine = new THREE.Group();
226
+ scene.add(machine);
227
+
228
+ const shellMat = new THREE.MeshStandardMaterial({ color: 0xdfe9f2, roughness: 0.35, metalness: 0.15 });
229
+ const darkMat = new THREE.MeshStandardMaterial({ color: 0x0a1929, roughness: 0.6, metalness: 0.3 });
230
+
231
+ // main gantry donut
232
+ const gantry = new THREE.Mesh(new THREE.TorusGeometry(1.95, 0.85, 32, 64), shellMat);
233
+ machine.add(gantry);
234
+ // housing block behind
235
+ const housing = new THREE.Mesh(new THREE.CylinderGeometry(2.6, 2.6, 1.6, 48), shellMat);
236
+ housing.rotation.x = Math.PI / 2;
237
+ housing.position.z = -1.3;
238
+ machine.add(housing);
239
+ // bore tube
240
+ const bore = new THREE.Mesh(
241
+ new THREE.CylinderGeometry(1.12, 1.12, 3.4, 48, 1, true),
242
+ new THREE.MeshStandardMaterial({ color: 0x060d16, roughness: .9, side: THREE.BackSide })
243
+ );
244
+ bore.rotation.x = Math.PI / 2;
245
+ bore.position.z = -0.6;
246
+ machine.add(bore);
247
+
248
+ // glowing ring inside the bore mouth
249
+ const glowMat = new THREE.MeshBasicMaterial({ color: 0x4fd6ff, transparent: true, opacity: 0.85 });
250
+ const ringLight = new THREE.Mesh(new THREE.TorusGeometry(1.13, 0.035, 12, 64), glowMat);
251
+ ringLight.position.z = 0.95;
252
+ machine.add(ringLight);
253
+ const ringLight2 = ringLight.clone(); ringLight2.position.z = -0.2; ringLight2.material = glowMat.clone();
254
+ ringLight2.material.opacity = 0.35; machine.add(ringLight2);
255
+
256
+ // patient table: pedestal + sliding tray
257
+ const pedestal = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.9, 4.6), darkMat);
258
+ pedestal.position.set(0, -1.62, 2.6);
259
+ scene.add(pedestal);
260
+ const tray = new THREE.Group();
261
+ const trayTop = new THREE.Mesh(new THREE.BoxGeometry(1.35, 0.09, 3.6), shellMat);
262
+ tray.add(trayTop);
263
+ tray.position.set(0, -1.12, 2.8);
264
+ scene.add(tray);
265
+
266
+ // floor glow disc
267
+ const floor = new THREE.Mesh(
268
+ new THREE.CircleGeometry(7, 48),
269
+ new THREE.MeshBasicMaterial({ color: 0x06121f, transparent: true, opacity: 0.85 })
270
+ );
271
+ floor.rotation.x = -Math.PI / 2; floor.position.y = -2.08; scene.add(floor);
272
+
273
+ /* --- the specimen plate (your image) --- */
274
+ const texLoader = new THREE.TextureLoader();
275
+ let specimenMesh = null;
276
+ function makeSpecimen(dataUrl, cb) {
277
+ texLoader.load(dataUrl, (tex) => {
278
+ tex.colorSpace = THREE.SRGBColorSpace;
279
+ const aspect = tex.image.width / tex.image.height;
280
+ // fit inside 1.5 wide x 1.3 tall, standing upright on the tray
281
+ let w = 1.5, h = w / aspect;
282
+ if (h > 1.3) { h = 1.3; w = h * aspect; }
283
+ if (specimenMesh) {
284
+ tray.remove(specimenMesh);
285
+ specimenMesh.geometry.dispose();
286
+ specimenMesh.material.dispose();
287
+ }
288
+ specimenMesh = new THREE.Mesh(
289
+ new THREE.PlaneGeometry(w, h),
290
+ new THREE.MeshBasicMaterial({ map: tex, side: THREE.DoubleSide, transparent: true })
291
+ );
292
+ specimenMesh.position.set(0, h / 2 + 0.05, 0.4);
293
+ tray.add(specimenMesh);
294
+ cb && cb();
295
+ });
296
+ }
297
+ function swapSpecimenTexture(dataUrl, cb) {
298
+ texLoader.load(dataUrl, (tex) => {
299
+ tex.colorSpace = THREE.SRGBColorSpace;
300
+ specimenMesh.material.map = tex;
301
+ specimenMesh.material.needsUpdate = true;
302
+ cb && cb();
303
+ });
304
+ }
305
+
306
+ // scan beam: glowing bar sweeping the bore mouth
307
+ const beam = new THREE.Mesh(
308
+ new THREE.PlaneGeometry(2.1, 0.045),
309
+ new THREE.MeshBasicMaterial({ color: 0x9ff0ff, transparent: true, opacity: 0, blending: THREE.AdditiveBlending })
310
+ );
311
+ beam.position.z = 1.0;
312
+ scene.add(beam);
313
+
314
+ // flash quad for the reveal moment
315
+ const flash = new THREE.Mesh(
316
+ new THREE.PlaneGeometry(30, 30),
317
+ new THREE.MeshBasicMaterial({ color: 0xcdf6ff, transparent: true, opacity: 0, blending: THREE.AdditiveBlending })
318
+ );
319
+ flash.position.z = 3.5;
320
+ scene.add(flash);
321
+
322
+ /* --- animation state machine --- */
323
+ const ease = (t) => t < .5 ? 2*t*t : -1 + (4 - 2*t) * t; // easeInOut
324
+ let mode = "idle"; // idle | in | scanning | out
325
+ let t0 = 0;
326
+ const TRAY_HOME = 2.8, TRAY_IN = -0.25;
327
+ let scanDone = false, revealData = null;
328
+
329
+ function setMode(m) { mode = m; t0 = performance.now(); }
330
+
331
+ let mouse = { x: 0, y: 0 };
332
+ window.addEventListener("pointermove", (e) => {
333
+ mouse.x = (e.clientX / window.innerWidth - 0.5) * 2;
334
+ mouse.y = (e.clientY / window.innerHeight - 0.5) * 2;
335
+ });
336
+
337
+ const clock = new THREE.Clock();
338
+ function animate() {
339
+ requestAnimationFrame(animate);
340
+ const t = clock.getElapsedTime();
341
+ const since = (performance.now() - t0) / 1000;
342
+
343
+ // idle ring pulse + slow spin
344
+ ringLight.rotation.z = t * 0.6;
345
+ ringLight2.rotation.z = -t * 0.4;
346
+ const basePulse = 0.75 + Math.sin(t * 2.2) * 0.15;
347
+
348
+ if (mode === "idle") {
349
+ glowMat.opacity = basePulse * 0.6;
350
+ beam.material.opacity = 0;
351
+ }
352
+
353
+ if (mode === "in") {
354
+ const p = ease(Math.min(since / 1.8, 1));
355
+ tray.position.z = TRAY_HOME + (TRAY_IN - TRAY_HOME) * p;
356
+ glowMat.opacity = 0.5 + p * 0.5;
357
+ if (p >= 1) setMode("scanning");
358
+ }
359
+
360
+ if (mode === "scanning") {
361
+ glowMat.opacity = 0.9 + Math.sin(t * 14) * 0.1;
362
+ ringLight.rotation.z = t * 5;
363
+ ringLight2.rotation.z = -t * 3.4;
364
+ // beam sweeps up and down across the bore mouth
365
+ const sweep = Math.sin(t * 2.6);
366
+ beam.position.y = sweep * 0.95;
367
+ beam.material.opacity = 0.55 + Math.sin(t * 9) * 0.2;
368
+ // when the scan result is in, flash and come out
369
+ if (scanDone && since > 4.2) {
370
+ swapSpecimenTexture(revealData.image, () => {
371
+ flash.material.opacity = 0.9;
372
+ setMode("out");
373
+ });
374
+ scanDone = false;
375
+ }
376
+ }
377
+
378
+ if (mode === "out") {
379
+ const p = ease(Math.min(since / 1.9, 1));
380
+ tray.position.z = TRAY_IN + (TRAY_HOME - TRAY_IN) * p;
381
+ beam.material.opacity = Math.max(0, 0.5 - p);
382
+ glowMat.opacity = 1 - p * 0.4;
383
+ ringLight.rotation.z = t * (5 - 4.4 * p);
384
+ if (p >= 1) { setMode("idle"); onScanFinished(); }
385
+ }
386
+
387
+ // flash decay
388
+ flash.material.opacity = Math.max(0, flash.material.opacity - 0.03);
389
+
390
+ // cinematic parallax
391
+ camera.position.x += ((mouse.x * 0.55) - camera.position.x) * 0.04;
392
+ camera.position.y += ((1.1 - mouse.y * 0.3) - camera.position.y) * 0.04;
393
+ camera.lookAt(0, -0.15, 0.6);
394
+
395
+ renderer.render(scene, camera);
396
+ }
397
+ resize();
398
+ animate();
399
+
400
+ /* ============ app logic ============ */
401
+ let specimenDataUrl = null;
402
+ let scanning = false;
403
+ let client = null;
404
 
405
  async function fileToDataUrl(file) {
406
  return new Promise((res) => {
 
410
  });
411
  }
412
 
413
+ function setSpecimen(dataUrl) {
414
+ if (scanning) return;
415
+ specimenDataUrl = dataUrl;
416
+ makeSpecimen(dataUrl, () => {
417
+ $("#scan-btn").disabled = false;
418
+ $("#stage-hint").style.display = "none";
419
+ clearLog();
420
+ log("// specimen on table β€” gantry ready", "#4fd6ff");
421
+ });
422
+ }
423
+
424
+ $("#load-btn").addEventListener("click", () => $("#file").click());
425
  $("#file").addEventListener("change", async (e) => {
426
  if (e.target.files[0]) setSpecimen(await fileToDataUrl(e.target.files[0]));
427
  });
 
437
  });
438
  });
439
 
440
+ /* scanner hum */
441
  let audioCtx = null, hum = null;
442
  function startHum() {
443
  try {
444
  audioCtx = audioCtx || new AudioContext();
445
+ const osc = audioCtx.createOscillator(), osc2 = audioCtx.createOscillator();
 
446
  const gain = audioCtx.createGain();
447
  osc.type = "sawtooth"; osc.frequency.value = 48;
448
  osc2.type = "sine"; osc2.frequency.value = 97;
449
+ gain.gain.value = 0.022;
450
  osc.connect(gain); osc2.connect(gain); gain.connect(audioCtx.destination);
451
  osc.start(); osc2.start();
452
  hum = { osc, osc2, gain };
453
+ } catch {}
454
  }
455
  function stopHum() {
456
  if (!hum) return;
457
+ hum.gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + .4);
458
  setTimeout(() => { hum.osc.stop(); hum.osc2.stop(); hum = null; }, 500);
459
  }
460
 
 
461
  const STAGES = [
462
+ "β—Œ TABLE ADVANCING INTO BORE",
463
  "β—Œ IDENTIFYING SPECIMEN",
464
  "β—Œ EXPOSING β€” 4-STEP RECTIFIED FLOW",
465
  "β—Œ RECONSTRUCTING SLICES",
466
  ];
467
 
468
+ let stageTimer = null;
469
  $("#scan-btn").addEventListener("click", async () => {
470
+ if (!specimenDataUrl || scanning || mode !== "idle") return;
471
  scanning = true;
472
+ scanDone = false; revealData = null;
473
  $("#scan-btn").disabled = true;
474
+ $("#view-btn").style.display = "none";
475
  $("#dl-btn").style.display = "none";
476
+ $("#scan-label").style.display = "block";
 
 
 
 
 
 
 
 
 
477
  clearLog();
478
+ log(STAGES[0] + " ...");
479
+ let stage = 1;
480
+ stageTimer = setInterval(() => {
481
  log(STAGES[Math.min(stage, STAGES.length - 1)] + " ...");
482
  stage++;
483
+ }, 2100);
484
+ startHum();
485
+ setMode("in");
486
 
487
  try {
488
  client = client || await Client.connect(location.origin);
489
  const out = await client.predict("/xray", { image_b64: specimenDataUrl });
490
+ revealData = out.data[0];
491
+ scanDone = true; // the animation loop picks this up and plays the reveal
 
 
 
 
 
 
 
 
 
492
  } catch (err) {
 
 
 
 
493
  clearInterval(stageTimer);
494
  stopHum();
495
+ $("#scan-label").style.display = "none";
496
+ setMode("out");
497
+ clearLog();
498
+ log("// SCAN FAULT β€” " + (err?.message || err), "#ff6b6b");
 
499
  scanning = false;
500
+ $("#scan-btn").disabled = false;
501
  }
502
  });
503
 
504
+ function onScanFinished() {
505
+ if (!revealData) return;
506
+ clearInterval(stageTimer);
507
+ stopHum();
508
+ $("#scan-label").style.display = "none";
509
+ clearLog();
510
+ log(`// SCAN COMPLETE β€” SPECIMEN: ${String(revealData.object).toUpperCase()}`, "#3dff9e");
511
+ log("// radiograph is an AI reconstruction, not engineering documentation", "#2f7ea8");
512
+ $("#view-btn").style.display = "block";
513
+ $("#dl-btn").style.display = "block";
514
+ const img = revealData.image;
515
+ $("#view-btn").onclick = () => {
516
+ $("#viewer-img").src = img;
517
+ $("#viewer").style.display = "flex";
518
+ };
519
+ $("#dl-btn").onclick = () => {
520
+ const a = document.createElement("a");
521
+ a.href = img; a.download = "radiograph.webp"; a.click();
522
+ };
523
+ scanning = false;
524
+ $("#scan-btn").disabled = false;
525
+ }
526
+ $("#viewer").addEventListener("click", () => { $("#viewer").style.display = "none"; });
527
  </script>
528
  </body>
529
  </html>