prithivMLmods commited on
Commit
9ef8327
Β·
verified Β·
1 Parent(s): 4592a59

update app

Browse files
Files changed (1) hide show
  1. app.py +220 -86
app.py CHANGED
@@ -94,7 +94,6 @@ def generate_inference_stream(image: Image.Image, category: str, prompt: str, mo
94
  full_prompt = prompt
95
 
96
  if model_id == "lfm":
97
- # LFM2.5-VL inference
98
  if lfm_model is None or lfm_processor is None:
99
  yield f"data: {json.dumps({'chunk': '[Error] LFM model not loaded.'})}\n\n"
100
  yield "data: [DONE]\n\n"
@@ -141,7 +140,6 @@ def generate_inference_stream(image: Image.Image, category: str, prompt: str, mo
141
  thread.join()
142
 
143
  else:
144
- # Qwen3.5 inference
145
  if qwen_model is None or qwen_processor is None:
146
  yield f"data: {json.dumps({'chunk': '[Error] Qwen model not loaded.'})}\n\n"
147
  yield "data: [DONE]\n\n"
@@ -241,24 +239,31 @@ async def homepage(request: Request):
241
  --wire-active: #7c6af7;
242
  }
243
 
244
- * { box-sizing: border-box; }
245
- body {
246
- margin: 0; padding: 0; overflow: hidden;
 
247
  background: var(--bg);
 
 
 
 
 
248
  background-image:
249
  radial-gradient(circle at 20% 50%, rgba(124,106,247,0.04) 0%, transparent 50%),
250
  radial-gradient(circle at 80% 20%, rgba(78,205,196,0.04) 0%, transparent 50%),
251
  linear-gradient(var(--grid) 1px, transparent 1px),
252
  linear-gradient(90deg, var(--grid) 1px, transparent 1px);
253
  background-size: 100% 100%, 100% 100%, 24px 24px, 24px 24px;
254
- color: var(--text);
255
- font-family: 'JetBrains Mono', monospace;
256
- user-select: none;
257
  }
258
 
259
  .top-bar {
260
- position: fixed; top: 0; left: 0; right: 0;
261
- height: 48px; background: rgba(13,13,15,0.9);
 
 
262
  border-bottom: 1px solid var(--node-border);
263
  display: flex; align-items: center; padding: 0 20px;
264
  gap: 12px; z-index: 1000;
@@ -268,21 +273,26 @@ async def homepage(request: Request):
268
  .top-bar .sep { color: var(--node-border); }
269
  .top-bar .sub { font-size: 11px; color: var(--muted); }
270
  .top-bar .badge {
271
- margin-left: auto; background: rgba(124,106,247,0.15);
272
- border: 1px solid rgba(124,106,247,0.3); padding: 3px 10px;
 
 
273
  border-radius: 20px; font-size: 10px; color: var(--accent);
274
  }
275
 
276
  #canvas {
277
- position: relative; width: 100vw; height: 100vh;
278
- padding-top: 48px;
279
- overflow: hidden;
 
 
280
  }
281
 
282
  svg.wires {
283
  position: absolute; top: 0; left: 0;
284
  width: 100%; height: 100%;
285
  pointer-events: none; z-index: 2;
 
286
  }
287
 
288
  path.wire {
@@ -313,10 +323,7 @@ async def homepage(request: Request):
313
  box-shadow: 0 12px 40px rgba(0,0,0,0.6), 0 0 0 1px rgba(124,106,247,0.3);
314
  }
315
 
316
- /* Fixed height nodes */
317
- .node.fixed-height {
318
- height: 420px;
319
- }
320
 
321
  .node-header {
322
  background: var(--node-header);
@@ -324,8 +331,10 @@ async def homepage(request: Request):
324
  border-bottom: 1px solid var(--node-border);
325
  border-radius: 10px 10px 0 0;
326
  font-size: 11px; font-weight: 700;
327
- cursor: grab; display: flex; justify-content: space-between; align-items: center;
 
328
  flex-shrink: 0;
 
329
  }
330
  .node-header:active { cursor: grabbing; }
331
  .node-header .id {
@@ -368,12 +377,14 @@ async def homepage(request: Request):
368
  font-size: 11px; color: var(--muted);
369
  transition: border-color 0.2s, background 0.2s;
370
  background: rgba(255,255,255,0.01);
 
371
  }
372
  .file-upload:hover { border-color: var(--accent); background: rgba(124,106,247,0.04); }
373
- .file-upload .icon { font-size: 22px; margin-bottom: 6px; }
 
374
 
375
  .img-preview {
376
- width: 100%; height: 190px;
377
  object-fit: contain;
378
  border-radius: 6px;
379
  display: none;
@@ -431,6 +442,7 @@ async def homepage(request: Request):
431
  .ground-canvas-wrap canvas {
432
  width: 100%; height: 100%;
433
  object-fit: contain;
 
434
  }
435
  .ground-placeholder {
436
  position: absolute; inset: 0;
@@ -449,7 +461,6 @@ async def homepage(request: Request):
449
  }
450
  @keyframes spin { to { transform: rotate(360deg); } }
451
 
452
- /* Status dot */
453
  .status-dot {
454
  width: 7px; height: 7px; border-radius: 50%;
455
  background: var(--muted); display: inline-block;
@@ -457,7 +468,6 @@ async def homepage(request: Request):
457
  }
458
  .status-dot.active { background: var(--accent2); box-shadow: 0 0 6px var(--accent2); }
459
 
460
- /* Model badge */
461
  .model-badge {
462
  display: inline-block; padding: 2px 8px;
463
  border-radius: 4px; font-size: 9px; font-weight: 700;
@@ -465,6 +475,11 @@ async def homepage(request: Request):
465
  }
466
  .model-badge.qwen { background: rgba(124,106,247,0.2); color: var(--accent); border: 1px solid rgba(124,106,247,0.3); }
467
  .model-badge.lfm { background: rgba(78,205,196,0.15); color: var(--accent2); border: 1px solid rgba(78,205,196,0.3); }
 
 
 
 
 
468
  </style>
469
  </head>
470
  <body>
@@ -478,17 +493,14 @@ async def homepage(request: Request):
478
 
479
  <div id="canvas">
480
  <svg class="wires">
481
- <!-- Left col β†’ Task node -->
482
  <path id="wire-img-task" class="wire" />
483
  <path id="wire-model-task" class="wire" />
484
- <!-- Task β†’ Output -->
485
  <path id="wire-task-out" class="wire" />
486
- <!-- Task β†’ Ground -->
487
  <path id="wire-task-gnd" class="wire" />
488
  </svg>
489
 
490
- <!-- ─── ID 01 : Image Input (left col, top) ─── -->
491
- <div class="node fixed-height" id="node-img" style="left:60px; top:68px;">
492
  <div class="node-header">
493
  <span><span class="status-dot" id="dot-img"></span>Input Image</span>
494
  <span class="id">ID: 01</span>
@@ -497,19 +509,22 @@ async def homepage(request: Request):
497
  <div>
498
  <label>Upload Image</label>
499
  <div class="file-upload" id="dropZone">
500
- <div class="icon">πŸ–ΌοΈ</div>
501
- Click or drop image here
 
 
 
 
502
  <input type="file" id="fileInput" accept="image/*">
503
  </div>
504
  <img id="imgPreview" class="img-preview" />
505
  </div>
506
  </div>
507
- <!-- OUT port: vertically centered on right edge -->
508
  <div class="port out" id="port-img-out" style="top:50%; transform:translateY(-50%);"></div>
509
  </div>
510
 
511
- <!-- ─── ID 02 : Model Selector (left col, bottom) ─── -->
512
- <div class="node fixed-height" id="node-model" style="left:60px; top:508px;">
513
  <div class="node-header">
514
  <span><span class="status-dot" id="dot-model"></span>Model Selector</span>
515
  <span class="id">ID: 02</span>
@@ -525,17 +540,15 @@ async def homepage(request: Request):
525
  <div id="modelInfoBox" style="background:rgba(124,106,247,0.07); border:1px solid rgba(124,106,247,0.2); border-radius:6px; padding:10px; font-size:10px; color:var(--muted); line-height:1.6;">
526
  <span class="model-badge qwen">QWEN</span>
527
  <br><br>
528
- Qwen3.5 2B parameter multimodal model by Alibaba Cloud. Supports Query, Caption, Point & Detect tasks with streaming output.
529
  </div>
530
  <div style="flex:1;"></div>
531
  </div>
532
- <!-- OUT port: vertically centered on right edge -->
533
  <div class="port out" id="port-model-out" style="top:50%; transform:translateY(-50%);"></div>
534
  </div>
535
 
536
- <!-- ─── ID 03 : Task Node (right col, row 1) ─── -->
537
- <div class="node fixed-height" id="node-task" style="left:460px; top:68px;">
538
- <!-- IN port aligned to accept wires from ID01 and ID02 -->
539
  <div class="port in" id="port-task-in" style="top:50%; transform:translateY(-50%);"></div>
540
  <div class="node-header">
541
  <span><span class="status-dot" id="dot-task"></span>Task Config</span>
@@ -560,12 +573,11 @@ async def homepage(request: Request):
560
  <span class="loader" id="btnLoader"></span>
561
  </button>
562
  </div>
563
- <!-- OUT port -->
564
  <div class="port out" id="port-task-out" style="top:50%; transform:translateY(-50%);"></div>
565
  </div>
566
 
567
- <!-- ─── ID 04 : Output Node (right col, row 1, further right) ─── -->
568
- <div class="node fixed-height" id="node-out" style="left:860px; top:68px;">
569
  <div class="port in" id="port-out-in" style="top:50%; transform:translateY(-50%);"></div>
570
  <div class="node-header">
571
  <span><span class="status-dot" id="dot-out"></span>Output Stream</span>
@@ -577,8 +589,8 @@ async def homepage(request: Request):
577
  </div>
578
  </div>
579
 
580
- <!-- ─── ID 05 : Grounding Visualiser (right col, row 2) ─── -->
581
- <div class="node fixed-height" id="node-gnd" style="left:860px; top:508px;">
582
  <div class="port in" id="port-gnd-in" style="top:50%; transform:translateY(-50%);"></div>
583
  <div class="node-header">
584
  <span><span class="status-dot" id="dot-gnd"></span>View Grounding</span>
@@ -594,17 +606,25 @@ async def homepage(request: Request):
594
  </div>
595
  </div>
596
  </div>
 
 
597
  </div>
598
 
599
  <script>
600
  // ══════════════════════════════════════════════
601
- // WIRE DRAWING
602
  // ══════════════════════════════════════════════
 
 
603
  function portCenter(id) {
604
  const el = document.getElementById(id);
605
- if (!el) return {x:0,y:0};
606
- const r = el.getBoundingClientRect();
607
- return { x: r.left + r.width/2, y: r.top + r.height/2 };
 
 
 
 
608
  }
609
 
610
  function bezier(p1, p2) {
@@ -634,8 +654,8 @@ document.querySelectorAll('.node').forEach(node => {
634
 
635
  header.addEventListener('mousedown', e => {
636
  drag = true; sx = e.clientX; sy = e.clientY;
637
- il = parseInt(node.style.left)||0;
638
- it = parseInt(node.style.top)||0;
639
  node.style.zIndex = 100;
640
  e.preventDefault();
641
  });
@@ -651,7 +671,8 @@ document.querySelectorAll('.node').forEach(node => {
651
  });
652
 
653
  window.addEventListener('resize', updateWires);
654
- // Delay first draw so layout is complete
 
655
  requestAnimationFrame(() => { updateWires(); });
656
 
657
  // ══════════════════════════════════════════════
@@ -670,15 +691,14 @@ function handleFile(file) {
670
  imgPreview.style.display = 'block';
671
  dropZone.style.display = 'none';
672
  dotImg.classList.add('active');
673
- // Wait for layout then redraw wires
674
  requestAnimationFrame(updateWires);
675
  }
676
 
677
- dropZone.onclick = () => fileInput.click();
678
- fileInput.onchange = e => handleFile(e.target.files[0]);
679
- dropZone.ondragover = e => { e.preventDefault(); dropZone.style.borderColor = 'var(--accent)'; };
680
- dropZone.ondragleave = ()=> { dropZone.style.borderColor = ''; };
681
- dropZone.ondrop = e => {
682
  e.preventDefault(); dropZone.style.borderColor = '';
683
  if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
684
  };
@@ -692,7 +712,7 @@ const dotModel = document.getElementById('dot-model');
692
  dotModel.classList.add('active');
693
 
694
  const MODEL_INFO = {
695
- qwen: `<span class="model-badge qwen">QWEN</span><br><br>Qwen3.5 2B parameter multimodal model by Alibaba Cloud. Supports Query, Caption, Point & Detect tasks with streaming output.`,
696
  lfm: `<span class="model-badge lfm">LFM</span><br><br>LFM2.5-VL 450M parameter vision-language model by LiquidAI. Ultra-lightweight edge model with strong grounding capabilities.`,
697
  };
698
 
@@ -715,6 +735,28 @@ categorySelect.onchange = e => {
715
  promptInput.placeholder = PLACEHOLDERS[e.target.value] || '';
716
  };
717
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
  // ══════════════════════════════════════════════
719
  // GROUNDING VISUALIZER
720
  // ══════════════════════════════════════════════
@@ -722,53 +764,145 @@ const groundCanvas = document.getElementById('groundCanvas');
722
  const groundPlaceholder = document.getElementById('groundPlaceholder');
723
  const gCtx = groundCanvas.getContext('2d');
724
 
 
 
 
 
 
 
 
 
 
725
  function drawGrounding(imgSrc, jsonText) {
726
  const parsed = safeParseJSON(jsonText);
727
- if (!parsed || (Array.isArray(parsed) && parsed.length === 0)) return;
 
 
 
728
 
729
  const img = new Image();
730
  img.onload = () => {
731
- groundCanvas.width = img.naturalWidth;
732
- groundCanvas.height = img.naturalHeight;
 
 
733
  gCtx.drawImage(img, 0, 0);
734
  groundPlaceholder.style.display = 'none';
735
 
736
- const W = img.naturalWidth, H = img.naturalHeight;
737
- gCtx.strokeStyle = '#4ecdc4';
738
- gCtx.lineWidth = Math.max(2, W/200);
739
- gCtx.fillStyle = 'rgba(78,205,196,0.25)';
740
- gCtx.font = `bold ${Math.max(12, W/40)}px JetBrains Mono, monospace`;
741
 
 
742
  const items = Array.isArray(parsed) ? parsed : [parsed];
 
743
  items.forEach((item, i) => {
744
- // Point format: [x, y] or {x, y}
745
- if (Array.isArray(item) && item.length === 2 && typeof item[0] === 'number') {
746
- const [x, y] = item;
747
- const px = x * W, py = y * H;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
  gCtx.beginPath();
749
- gCtx.arc(px, py, Math.max(8, W/60), 0, Math.PI*2);
 
750
  gCtx.fill();
 
751
  gCtx.stroke();
752
- }
753
- // BBox format: [x1,y1,x2,y2] or {x1,y1,x2,y2}
754
- if (Array.isArray(item) && item.length === 4) {
755
- const [x1,y1,x2,y2] = item;
756
- const bx = x1*W, by = y1*H, bw = (x2-x1)*W, bh = (y2-y1)*H;
757
- gCtx.fillRect(bx, by, bw, bh);
758
- gCtx.strokeRect(bx, by, bw, bh);
759
- gCtx.fillStyle = '#4ecdc4';
760
- gCtx.fillText(`${i+1}`, bx+4, by+gCtx.font.match(/\d+/)[0]*1.1);
761
- gCtx.fillStyle = 'rgba(78,205,196,0.25)';
762
  }
763
  });
764
  };
765
  img.src = imgSrc;
766
  }
767
 
768
- function safeParseJSON(text) {
769
- text = text.trim().replace(/^```(json)?/, '').replace(/```$/, '').trim();
770
- try { return JSON.parse(text); } catch(_){}
771
- try { return eval('(' + text + ')'); } catch(_) { return null; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  }
773
 
774
  // ══════════════════════════════════════════════
@@ -841,7 +975,7 @@ runBtn.onclick = async () => {
841
 
842
  dotOut.classList.add('active');
843
 
844
- // Visualise grounding if applicable
845
  const cat = categorySelect.value;
846
  if ((cat === 'Point' || cat === 'Detect') && fullText.trim()) {
847
  dotGnd.classList.add('active');
 
94
  full_prompt = prompt
95
 
96
  if model_id == "lfm":
 
97
  if lfm_model is None or lfm_processor is None:
98
  yield f"data: {json.dumps({'chunk': '[Error] LFM model not loaded.'})}\n\n"
99
  yield "data: [DONE]\n\n"
 
140
  thread.join()
141
 
142
  else:
 
143
  if qwen_model is None or qwen_processor is None:
144
  yield f"data: {json.dumps({'chunk': '[Error] Qwen model not loaded.'})}\n\n"
145
  yield "data: [DONE]\n\n"
 
239
  --wire-active: #7c6af7;
240
  }
241
 
242
+ * { box-sizing: border-box; margin: 0; padding: 0; }
243
+
244
+ html, body {
245
+ min-height: 100%;
246
  background: var(--bg);
247
+ color: var(--text);
248
+ font-family: 'JetBrains Mono', monospace;
249
+ }
250
+
251
+ body {
252
  background-image:
253
  radial-gradient(circle at 20% 50%, rgba(124,106,247,0.04) 0%, transparent 50%),
254
  radial-gradient(circle at 80% 20%, rgba(78,205,196,0.04) 0%, transparent 50%),
255
  linear-gradient(var(--grid) 1px, transparent 1px),
256
  linear-gradient(90deg, var(--grid) 1px, transparent 1px);
257
  background-size: 100% 100%, 100% 100%, 24px 24px, 24px 24px;
258
+ overflow-x: auto;
259
+ overflow-y: auto;
 
260
  }
261
 
262
  .top-bar {
263
+ position: sticky;
264
+ top: 0; left: 0; right: 0;
265
+ height: 48px;
266
+ background: rgba(13,13,15,0.95);
267
  border-bottom: 1px solid var(--node-border);
268
  display: flex; align-items: center; padding: 0 20px;
269
  gap: 12px; z-index: 1000;
 
273
  .top-bar .sep { color: var(--node-border); }
274
  .top-bar .sub { font-size: 11px; color: var(--muted); }
275
  .top-bar .badge {
276
+ margin-left: auto;
277
+ background: rgba(124,106,247,0.15);
278
+ border: 1px solid rgba(124,106,247,0.3);
279
+ padding: 3px 10px;
280
  border-radius: 20px; font-size: 10px; color: var(--accent);
281
  }
282
 
283
  #canvas {
284
+ position: relative;
285
+ width: 1340px;
286
+ min-height: calc(100vh - 48px);
287
+ height: 1000px;
288
+ margin: 0 auto;
289
  }
290
 
291
  svg.wires {
292
  position: absolute; top: 0; left: 0;
293
  width: 100%; height: 100%;
294
  pointer-events: none; z-index: 2;
295
+ overflow: visible;
296
  }
297
 
298
  path.wire {
 
323
  box-shadow: 0 12px 40px rgba(0,0,0,0.6), 0 0 0 1px rgba(124,106,247,0.3);
324
  }
325
 
326
+ .node.fixed-height { height: 420px; }
 
 
 
327
 
328
  .node-header {
329
  background: var(--node-header);
 
331
  border-bottom: 1px solid var(--node-border);
332
  border-radius: 10px 10px 0 0;
333
  font-size: 11px; font-weight: 700;
334
+ cursor: grab;
335
+ display: flex; justify-content: space-between; align-items: center;
336
  flex-shrink: 0;
337
+ user-select: none;
338
  }
339
  .node-header:active { cursor: grabbing; }
340
  .node-header .id {
 
377
  font-size: 11px; color: var(--muted);
378
  transition: border-color 0.2s, background 0.2s;
379
  background: rgba(255,255,255,0.01);
380
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
381
  }
382
  .file-upload:hover { border-color: var(--accent); background: rgba(124,106,247,0.04); }
383
+ .file-upload svg { opacity: 0.5; transition: opacity 0.2s; }
384
+ .file-upload:hover svg { opacity: 0.9; }
385
 
386
  .img-preview {
387
+ width: 100%; height: 230px;
388
  object-fit: contain;
389
  border-radius: 6px;
390
  display: none;
 
442
  .ground-canvas-wrap canvas {
443
  width: 100%; height: 100%;
444
  object-fit: contain;
445
+ display: block;
446
  }
447
  .ground-placeholder {
448
  position: absolute; inset: 0;
 
461
  }
462
  @keyframes spin { to { transform: rotate(360deg); } }
463
 
 
464
  .status-dot {
465
  width: 7px; height: 7px; border-radius: 50%;
466
  background: var(--muted); display: inline-block;
 
468
  }
469
  .status-dot.active { background: var(--accent2); box-shadow: 0 0 6px var(--accent2); }
470
 
 
471
  .model-badge {
472
  display: inline-block; padding: 2px 8px;
473
  border-radius: 4px; font-size: 9px; font-weight: 700;
 
475
  }
476
  .model-badge.qwen { background: rgba(124,106,247,0.2); color: var(--accent); border: 1px solid rgba(124,106,247,0.3); }
477
  .model-badge.lfm { background: rgba(78,205,196,0.15); color: var(--accent2); border: 1px solid rgba(78,205,196,0.3); }
478
+
479
+ /* scroll hint at bottom */
480
+ .canvas-footer {
481
+ height: 40px;
482
+ }
483
  </style>
484
  </head>
485
  <body>
 
493
 
494
  <div id="canvas">
495
  <svg class="wires">
 
496
  <path id="wire-img-task" class="wire" />
497
  <path id="wire-model-task" class="wire" />
 
498
  <path id="wire-task-out" class="wire" />
 
499
  <path id="wire-task-gnd" class="wire" />
500
  </svg>
501
 
502
+ <!-- ─── ID 01 : Image Input ─── -->
503
+ <div class="node fixed-height" id="node-img" style="left:40px; top:60px;">
504
  <div class="node-header">
505
  <span><span class="status-dot" id="dot-img"></span>Input Image</span>
506
  <span class="id">ID: 01</span>
 
509
  <div>
510
  <label>Upload Image</label>
511
  <div class="file-upload" id="dropZone">
512
+ <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#7c6af7" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
513
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
514
+ <circle cx="8.5" cy="8.5" r="1.5"/>
515
+ <polyline points="21 15 16 10 5 21"/>
516
+ </svg>
517
+ <span>Click or drop image here</span>
518
  <input type="file" id="fileInput" accept="image/*">
519
  </div>
520
  <img id="imgPreview" class="img-preview" />
521
  </div>
522
  </div>
 
523
  <div class="port out" id="port-img-out" style="top:50%; transform:translateY(-50%);"></div>
524
  </div>
525
 
526
+ <!-- ─── ID 02 : Model Selector ─── -->
527
+ <div class="node fixed-height" id="node-model" style="left:40px; top:500px;">
528
  <div class="node-header">
529
  <span><span class="status-dot" id="dot-model"></span>Model Selector</span>
530
  <span class="id">ID: 02</span>
 
540
  <div id="modelInfoBox" style="background:rgba(124,106,247,0.07); border:1px solid rgba(124,106,247,0.2); border-radius:6px; padding:10px; font-size:10px; color:var(--muted); line-height:1.6;">
541
  <span class="model-badge qwen">QWEN</span>
542
  <br><br>
543
+ Qwen3.5 2B parameter multimodal model by Alibaba Cloud. Supports Query, Caption, Point &amp; Detect tasks with streaming output.
544
  </div>
545
  <div style="flex:1;"></div>
546
  </div>
 
547
  <div class="port out" id="port-model-out" style="top:50%; transform:translateY(-50%);"></div>
548
  </div>
549
 
550
+ <!-- ─── ID 03 : Task Node ─── -->
551
+ <div class="node fixed-height" id="node-task" style="left:430px; top:60px;">
 
552
  <div class="port in" id="port-task-in" style="top:50%; transform:translateY(-50%);"></div>
553
  <div class="node-header">
554
  <span><span class="status-dot" id="dot-task"></span>Task Config</span>
 
573
  <span class="loader" id="btnLoader"></span>
574
  </button>
575
  </div>
 
576
  <div class="port out" id="port-task-out" style="top:50%; transform:translateY(-50%);"></div>
577
  </div>
578
 
579
+ <!-- ─── ID 04 : Output Node ─── -->
580
+ <div class="node fixed-height" id="node-out" style="left:820px; top:60px;">
581
  <div class="port in" id="port-out-in" style="top:50%; transform:translateY(-50%);"></div>
582
  <div class="node-header">
583
  <span><span class="status-dot" id="dot-out"></span>Output Stream</span>
 
589
  </div>
590
  </div>
591
 
592
+ <!-- ─── ID 05 : Grounding Visualiser ─── -->
593
+ <div class="node fixed-height" id="node-gnd" style="left:820px; top:500px;">
594
  <div class="port in" id="port-gnd-in" style="top:50%; transform:translateY(-50%);"></div>
595
  <div class="node-header">
596
  <span><span class="status-dot" id="dot-gnd"></span>View Grounding</span>
 
606
  </div>
607
  </div>
608
  </div>
609
+
610
+ <div class="canvas-footer"></div>
611
  </div>
612
 
613
  <script>
614
  // ══════════════════════════════════════════════
615
+ // WIRE DRAWING (relative to #canvas)
616
  // ══════════════════════════════════════════════
617
+ const canvasEl = document.getElementById('canvas');
618
+
619
  function portCenter(id) {
620
  const el = document.getElementById(id);
621
+ if (!el) return { x: 0, y: 0 };
622
+ const er = el.getBoundingClientRect();
623
+ const cr = canvasEl.getBoundingClientRect();
624
+ return {
625
+ x: er.left + er.width / 2 - cr.left,
626
+ y: er.top + er.height / 2 - cr.top
627
+ };
628
  }
629
 
630
  function bezier(p1, p2) {
 
654
 
655
  header.addEventListener('mousedown', e => {
656
  drag = true; sx = e.clientX; sy = e.clientY;
657
+ il = parseInt(node.style.left) || 0;
658
+ it = parseInt(node.style.top) || 0;
659
  node.style.zIndex = 100;
660
  e.preventDefault();
661
  });
 
671
  });
672
 
673
  window.addEventListener('resize', updateWires);
674
+ window.addEventListener('scroll', updateWires);
675
+ document.addEventListener('scroll', updateWires, true);
676
  requestAnimationFrame(() => { updateWires(); });
677
 
678
  // ══════════════════════════════════════════════
 
691
  imgPreview.style.display = 'block';
692
  dropZone.style.display = 'none';
693
  dotImg.classList.add('active');
 
694
  requestAnimationFrame(updateWires);
695
  }
696
 
697
+ dropZone.onclick = () => fileInput.click();
698
+ fileInput.onchange = e => handleFile(e.target.files[0]);
699
+ dropZone.ondragover = e => { e.preventDefault(); dropZone.style.borderColor = 'var(--accent)'; };
700
+ dropZone.ondragleave = () => { dropZone.style.borderColor = ''; };
701
+ dropZone.ondrop = e => {
702
  e.preventDefault(); dropZone.style.borderColor = '';
703
  if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
704
  };
 
712
  dotModel.classList.add('active');
713
 
714
  const MODEL_INFO = {
715
+ qwen: `<span class="model-badge qwen">QWEN</span><br><br>Qwen3.5 2B parameter multimodal model by Alibaba Cloud. Supports Query, Caption, Point &amp; Detect tasks with streaming output.`,
716
  lfm: `<span class="model-badge lfm">LFM</span><br><br>LFM2.5-VL 450M parameter vision-language model by LiquidAI. Ultra-lightweight edge model with strong grounding capabilities.`,
717
  };
718
 
 
735
  promptInput.placeholder = PLACEHOLDERS[e.target.value] || '';
736
  };
737
 
738
+ // ══════════════════════════════════════════════
739
+ // JSON PARSER (robust)
740
+ // ══════════════════════════════════════════════
741
+ function safeParseJSON(text) {
742
+ // Strip markdown fences
743
+ text = text.trim()
744
+ .replace(/^```(json)?\\s*/i, '')
745
+ .replace(/\\s*```$/, '')
746
+ .trim();
747
+ try { return JSON.parse(text); } catch(_) {}
748
+ // Try to extract first JSON array or object
749
+ const arrMatch = text.match(/\\[\\s*[\\s\\S]*?\\]/);
750
+ if (arrMatch) {
751
+ try { return JSON.parse(arrMatch[0]); } catch(_) {}
752
+ }
753
+ const objMatch = text.match(/\\{[\\s\\S]*?\\}/);
754
+ if (objMatch) {
755
+ try { return JSON.parse(objMatch[0]); } catch(_) {}
756
+ }
757
+ return null;
758
+ }
759
+
760
  // ══════════════════════════════════════════════
761
  // GROUNDING VISUALIZER
762
  // ══════════════════════════════════════════════
 
764
  const groundPlaceholder = document.getElementById('groundPlaceholder');
765
  const gCtx = groundCanvas.getContext('2d');
766
 
767
+ /*
768
+ Handles all common model output formats:
769
+ bbox_2d : [x1,y1,x2,y2] β€” pixel or normalised
770
+ bbox : [x1,y1,x2,y2]
771
+ point_2d: [x,y] β€” pixel or normalised
772
+ point : [x,y]
773
+ raw arrays of 4 numbers β†’ bbox
774
+ raw arrays of 2 numbers β†’ point
775
+ */
776
  function drawGrounding(imgSrc, jsonText) {
777
  const parsed = safeParseJSON(jsonText);
778
+ if (!parsed) {
779
+ console.warn('Grounding: could not parse JSON:', jsonText);
780
+ return;
781
+ }
782
 
783
  const img = new Image();
784
  img.onload = () => {
785
+ const W = img.naturalWidth;
786
+ const H = img.naturalHeight;
787
+ groundCanvas.width = W;
788
+ groundCanvas.height = H;
789
  gCtx.drawImage(img, 0, 0);
790
  groundPlaceholder.style.display = 'none';
791
 
792
+ const lw = Math.max(2, W / 200);
793
+ const fs = Math.max(12, W / 40);
794
+ gCtx.lineWidth = lw;
795
+ gCtx.font = `bold ${fs}px JetBrains Mono, monospace`;
 
796
 
797
+ // Normalise to array of items
798
  const items = Array.isArray(parsed) ? parsed : [parsed];
799
+
800
  items.forEach((item, i) => {
801
+ // ── Detect / bbox ──
802
+ let bbox = null;
803
+ if (item && item.bbox_2d && Array.isArray(item.bbox_2d) && item.bbox_2d.length === 4) {
804
+ bbox = item.bbox_2d;
805
+ } else if (item && item.bbox && Array.isArray(item.bbox) && item.bbox.length === 4) {
806
+ bbox = item.bbox;
807
+ } else if (Array.isArray(item) && item.length === 4 && item.every(n => typeof n === 'number')) {
808
+ bbox = item;
809
+ }
810
+
811
+ if (bbox) {
812
+ let [x1, y1, x2, y2] = bbox;
813
+ // Auto-detect pixel vs normalised (values > 2 β†’ pixel coords)
814
+ const isNorm = x1 <= 1 && y1 <= 1 && x2 <= 1 && y2 <= 1;
815
+ if (isNorm) { x1*=W; y1*=H; x2*=W; y2*=H; }
816
+ const bw = x2 - x1, bh = y2 - y1;
817
+ const label = item.label || `${i+1}`;
818
+ const colors = [
819
+ '#4ecdc4','#7c6af7','#ff6b6b','#ffd93d',
820
+ '#6bcb77','#ff922b','#cc5de8','#339af0'
821
+ ];
822
+ const col = colors[i % colors.length];
823
+
824
+ // Fill
825
+ gCtx.fillStyle = col.replace(/^#/,'') === col
826
+ ? col + '33'
827
+ : hexToRgba(col, 0.18);
828
+ gCtx.fillRect(x1, y1, bw, bh);
829
+
830
+ // Stroke
831
+ gCtx.strokeStyle = col;
832
+ gCtx.strokeRect(x1, y1, bw, bh);
833
+
834
+ // Label pill
835
+ const textW = gCtx.measureText(label).width;
836
+ const ph = fs * 1.4, pw = textW + 10;
837
+ const lx = x1, ly = Math.max(0, y1 - ph);
838
+ gCtx.fillStyle = col;
839
+ roundRect(gCtx, lx, ly, pw, ph, 4);
840
+ gCtx.fill();
841
+ gCtx.fillStyle = '#fff';
842
+ gCtx.fillText(label, lx + 5, ly + ph * 0.75);
843
+ return;
844
+ }
845
+
846
+ // ── Point ──
847
+ let pt = null;
848
+ if (item && item.point_2d && Array.isArray(item.point_2d) && item.point_2d.length === 2) {
849
+ pt = item.point_2d;
850
+ } else if (item && item.point && Array.isArray(item.point) && item.point.length === 2) {
851
+ pt = item.point;
852
+ } else if (Array.isArray(item) && item.length === 2 && item.every(n => typeof n === 'number')) {
853
+ pt = item;
854
+ }
855
+
856
+ if (pt) {
857
+ let [x, y] = pt;
858
+ const isNorm = x <= 1 && y <= 1;
859
+ if (isNorm) { x *= W; y *= H; }
860
+ const r = Math.max(8, W / 60);
861
+ const col = '#4ecdc4';
862
+
863
+ // Outer ring
864
+ gCtx.beginPath();
865
+ gCtx.arc(x, y, r * 1.6, 0, Math.PI * 2);
866
+ gCtx.fillStyle = 'rgba(78,205,196,0.15)';
867
+ gCtx.fill();
868
+
869
+ // Main dot
870
  gCtx.beginPath();
871
+ gCtx.arc(x, y, r, 0, Math.PI * 2);
872
+ gCtx.fillStyle = col;
873
  gCtx.fill();
874
+ gCtx.strokeStyle = '#fff';
875
  gCtx.stroke();
876
+
877
+ // Label
878
+ const label = item.label || `${i+1}`;
879
+ gCtx.fillStyle = '#fff';
880
+ gCtx.fillText(label, x + r + 4, y + fs * 0.4);
 
 
 
 
 
881
  }
882
  });
883
  };
884
  img.src = imgSrc;
885
  }
886
 
887
+ function hexToRgba(hex, alpha) {
888
+ const r = parseInt(hex.slice(1,3),16);
889
+ const g = parseInt(hex.slice(3,5),16);
890
+ const b = parseInt(hex.slice(5,7),16);
891
+ return `rgba(${r},${g},${b},${alpha})`;
892
+ }
893
+
894
+ function roundRect(ctx, x, y, w, h, r) {
895
+ ctx.beginPath();
896
+ ctx.moveTo(x + r, y);
897
+ ctx.lineTo(x + w - r, y);
898
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
899
+ ctx.lineTo(x + w, y + h - r);
900
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
901
+ ctx.lineTo(x + r, y + h);
902
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
903
+ ctx.lineTo(x, y + r);
904
+ ctx.quadraticCurveTo(x, y, x + r, y);
905
+ ctx.closePath();
906
  }
907
 
908
  // ══════════════════════════════════════════════
 
975
 
976
  dotOut.classList.add('active');
977
 
978
+ // Visualise grounding
979
  const cat = categorySelect.value;
980
  if ((cat === 'Point' || cat === 'Detect') && fullText.trim()) {
981
  dotGnd.classList.add('active');