prithivMLmods commited on
Commit
dd6fcaa
Β·
verified Β·
1 Parent(s): c212f06

update app

Browse files
Files changed (1) hide show
  1. app.py +295 -532
app.py CHANGED
@@ -33,11 +33,12 @@ DTYPE = (
33
  else torch.float16
34
  )
35
 
36
- QWEN_4B_MODEL_NAME = "Qwen/Qwen3.5-4B"
37
- QWEN_2B_MODEL_NAME = "Qwen/Qwen3.5-2B"
38
- QWEN_VL_MODEL_NAME = "Qwen/Qwen3-VL-2B-Instruct"
39
- LFM_450_MODEL_NAME = "LiquidAI/LFM2.5-VL-450M"
40
- LFM_16_MODEL_NAME = "LiquidAI/LFM2.5-VL-1.6B"
 
41
 
42
  # ── Qwen3.5-4B ──────────────────────────────────────────
43
  print(f"Loading Qwen3.5-4B model: {QWEN_4B_MODEL_NAME} on {DEVICE}...")
@@ -112,6 +113,19 @@ except Exception as e:
112
  lfm_16_model = None
113
  lfm_16_processor = None
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  # --- Utility Functions ---
117
  def safe_parse_json(text: str):
@@ -151,6 +165,7 @@ def generate_inference_stream(
151
  yield f"data: {json.dumps({'chunk': '[Error] Qwen3.5-4B model not loaded.'})}\n\n"
152
  yield "data: [DONE]\n\n"
153
  return
 
154
  messages = [{"role": "user", "content": [
155
  {"type": "image", "image": image},
156
  {"type": "text", "text": full_prompt},
@@ -182,6 +197,7 @@ def generate_inference_stream(
182
  yield f"data: {json.dumps({'chunk': '[Error] Qwen3.5-2B model not loaded.'})}\n\n"
183
  yield "data: [DONE]\n\n"
184
  return
 
185
  messages = [{"role": "user", "content": [
186
  {"type": "image", "image": image},
187
  {"type": "text", "text": full_prompt},
@@ -213,6 +229,7 @@ def generate_inference_stream(
213
  yield f"data: {json.dumps({'chunk': '[Error] Qwen3-VL model not loaded.'})}\n\n"
214
  yield "data: [DONE]\n\n"
215
  return
 
216
  messages = [{"role": "user", "content": [
217
  {"type": "image", "image": image},
218
  {"type": "text", "text": full_prompt},
@@ -244,6 +261,7 @@ def generate_inference_stream(
244
  yield f"data: {json.dumps({'chunk': '[Error] LFM-450M model not loaded.'})}\n\n"
245
  yield "data: [DONE]\n\n"
246
  return
 
247
  conversation = [{"role": "user", "content": [
248
  {"type": "image", "image": image},
249
  {"type": "text", "text": full_prompt},
@@ -272,6 +290,7 @@ def generate_inference_stream(
272
  yield f"data: {json.dumps({'chunk': '[Error] LFM-1.6B model not loaded.'})}\n\n"
273
  yield "data: [DONE]\n\n"
274
  return
 
275
  conversation = [{"role": "user", "content": [
276
  {"type": "image", "image": image},
277
  {"type": "text", "text": full_prompt},
@@ -294,6 +313,38 @@ def generate_inference_stream(
294
  yield f"data: {json.dumps({'chunk': tok})}\n\n"
295
  thread.join()
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  yield "data: [DONE]\n\n"
298
 
299
 
@@ -348,55 +399,51 @@ async def homepage(request: Request):
348
  * { box-sizing: border-box; margin: 0; padding: 0; }
349
 
350
  html, body {
351
- width: 100%; height: 100%;
352
- overflow: hidden;
353
  background: var(--bg);
354
  color: var(--text);
355
  font-family: 'JetBrains Mono', monospace;
356
  }
357
 
 
 
 
 
 
 
 
 
 
 
 
358
  /* ── Top Bar ── */
359
  .top-bar {
360
- position: fixed; top: 0; left: 0; right: 0;
361
- height: 42px;
362
- background: rgba(13,13,15,0.97);
363
  border-bottom: 1px solid var(--node-border);
364
- display: flex; align-items: center; padding: 0 20px;
365
- gap: 12px; z-index: 2000;
366
  backdrop-filter: blur(12px);
367
  }
368
- .top-bar .logo { font-size: 13px; font-weight: 700; color: var(--accent); letter-spacing: 0.05em; }
369
- .top-bar .sep { color: var(--node-border); }
370
- .top-bar .sub { font-size: 11px; color: var(--muted); }
371
  .top-bar .badge {
372
  margin-left: auto;
373
  background: rgba(124,106,247,0.15);
374
  border: 1px solid rgba(124,106,247,0.3);
375
- padding: 3px 10px; border-radius: 20px;
376
- font-size: 10px; color: var(--accent);
377
  }
378
 
379
- /* ── Viewport (the scrollable/zoomable area) ── */
380
- #viewport {
381
- position: fixed;
382
- top: 42px; left: 0; right: 0; bottom: 0;
383
- overflow: hidden;
384
- cursor: default;
385
- }
386
-
387
- /* ── World (the zoomable container) ── */
388
- #world {
389
- position: absolute;
390
- top: 0; left: 0;
391
- width: 2400px;
392
- height: 1600px;
393
- transform-origin: 0 0;
394
- background-image:
395
- radial-gradient(circle at 20% 50%, rgba(124,106,247,0.04) 0%, transparent 50%),
396
- radial-gradient(circle at 80% 20%, rgba(78,205,196,0.04) 0%, transparent 50%),
397
- linear-gradient(var(--grid) 1px, transparent 1px),
398
- linear-gradient(90deg, var(--grid) 1px, transparent 1px);
399
- background-size: 100% 100%, 100% 100%, 24px 24px, 24px 24px;
400
  }
401
 
402
  svg.wires {
@@ -419,26 +466,26 @@ async def homepage(request: Request):
419
  /* ── Nodes ── */
420
  .node {
421
  position: absolute;
422
- width: 295px;
423
  background: var(--node-bg);
424
  border: 1px solid var(--node-border);
425
- border-radius: 9px;
426
- box-shadow: 0 8px 28px rgba(0,0,0,0.5);
427
  z-index: 10;
428
  display: flex; flex-direction: column;
429
  transition: box-shadow 0.2s;
430
  }
431
  .node:hover {
432
- box-shadow: 0 8px 28px rgba(0,0,0,0.5), 0 0 0 1px rgba(124,106,247,0.3);
433
  }
434
- .node.fixed-height { height: 330px; }
435
 
436
  .node-header {
437
  background: var(--node-header);
438
- padding: 7px 12px;
439
  border-bottom: 1px solid var(--node-border);
440
- border-radius: 9px 9px 0 0;
441
- font-size: 11px; font-weight: 700;
442
  cursor: grab;
443
  display: flex; justify-content: space-between; align-items: center;
444
  flex-shrink: 0;
@@ -446,32 +493,32 @@ async def homepage(request: Request):
446
  }
447
  .node-header:active { cursor: grabbing; }
448
  .node-header .id {
449
- font-size: 10px; color: var(--muted);
450
  background: rgba(255,255,255,0.04);
451
- padding: 2px 7px; border-radius: 4px;
452
  }
453
 
454
  .node-body {
455
- padding: 10px;
456
- display: flex; flex-direction: column; gap: 8px;
457
  flex: 1; overflow: hidden;
458
  }
459
 
460
  /* ── Ports ── */
461
  .port {
462
  position: absolute;
463
- width: 11px; height: 11px;
464
  background: var(--node-bg);
465
  border: 2px solid var(--port);
466
  border-radius: 50%; z-index: 30;
467
  }
468
- .port.out { right: -6px; }
469
- .port.in { left: -6px; }
470
 
471
  /* ── Labels ── */
472
  label {
473
- font-size: 10px; color: var(--muted);
474
- font-weight: 600; display: block; margin-bottom: 3px;
475
  letter-spacing: 0.07em; text-transform: uppercase;
476
  }
477
 
@@ -479,20 +526,20 @@ async def homepage(request: Request):
479
 
480
  .file-upload {
481
  border: 1.5px dashed var(--node-border);
482
- border-radius: 7px; padding: 12px 10px;
483
  text-align: center; cursor: pointer;
484
- font-size: 11px; color: var(--muted);
485
  transition: border-color 0.2s, background 0.2s;
486
  background: rgba(255,255,255,0.01);
487
- display: flex; flex-direction: column; align-items: center; gap: 5px;
488
  }
489
  .file-upload:hover { border-color: var(--accent); background: rgba(124,106,247,0.04); }
490
  .file-upload svg { opacity: 0.5; transition: opacity 0.2s; }
491
  .file-upload:hover svg { opacity: 0.9; }
492
 
493
  .img-preview {
494
- width: 100%; height: 170px;
495
- object-fit: contain; border-radius: 5px;
496
  display: none; background: #000;
497
  border: 1px solid var(--node-border);
498
  }
@@ -501,9 +548,9 @@ async def homepage(request: Request):
501
  width: 100%;
502
  background: rgba(0,0,0,0.3);
503
  border: 1px solid var(--node-border);
504
- color: var(--text); padding: 7px 9px;
505
- border-radius: 5px; outline: none;
506
- font-size: 11px; font-family: 'JetBrains Mono', monospace;
507
  resize: none; transition: border-color 0.2s;
508
  }
509
  select:focus, textarea:focus { border-color: var(--accent); }
@@ -512,12 +559,12 @@ async def homepage(request: Request):
512
  button.run-btn {
513
  background: linear-gradient(135deg, var(--accent), #9b59b6);
514
  color: #fff; border: none;
515
- padding: 8px; border-radius: 6px;
516
- font-weight: 700; font-size: 11px;
517
  font-family: 'JetBrains Mono', monospace;
518
  cursor: pointer;
519
  transition: opacity 0.2s, transform 0.1s;
520
- display: flex; justify-content: center; align-items: center; gap: 8px;
521
  letter-spacing: 0.04em; flex-shrink: 0;
522
  }
523
  button.run-btn:hover { opacity: 0.9; }
@@ -527,9 +574,9 @@ async def homepage(request: Request):
527
  .output-box {
528
  background: rgba(0,0,0,0.4);
529
  border: 1px solid var(--node-border);
530
- border-radius: 5px; padding: 10px;
531
  flex: 1; overflow-y: auto;
532
- font-size: 11px; line-height: 1.6;
533
  color: #c8c8e0; white-space: pre-wrap;
534
  user-select: text;
535
  font-family: 'JetBrains Mono', monospace;
@@ -539,7 +586,7 @@ async def homepage(request: Request):
539
  .ground-canvas-wrap {
540
  position: relative; flex: 1;
541
  border: 1px solid var(--node-border);
542
- border-radius: 5px; overflow: hidden;
543
  background: #000; min-height: 0;
544
  }
545
  .ground-canvas-wrap canvas {
@@ -549,11 +596,11 @@ async def homepage(request: Request):
549
  .ground-placeholder {
550
  position: absolute; inset: 0;
551
  display: flex; align-items: center; justify-content: center;
552
- font-size: 11px; color: var(--muted); text-align: center; padding: 10px;
553
  }
554
 
555
  .loader {
556
- width: 11px; height: 11px;
557
  border: 2px solid rgba(255,255,255,0.3);
558
  border-top-color: #fff; border-radius: 50%;
559
  animation: spin 0.7s linear infinite;
@@ -562,15 +609,18 @@ async def homepage(request: Request):
562
  @keyframes spin { to { transform: rotate(360deg); } }
563
 
564
  .status-dot {
565
- width: 6px; height: 6px; border-radius: 50%;
566
- background: var(--muted); display: inline-block; margin-right: 6px;
 
 
 
 
567
  }
568
- .status-dot.active { background: var(--accent2); box-shadow: 0 0 5px var(--accent2); }
569
 
570
  /* ── Model badges ── */
571
  .model-badge {
572
- display: inline-block; padding: 2px 7px;
573
- border-radius: 4px; font-size: 9px; font-weight: 700;
574
  letter-spacing: 0.06em; text-transform: uppercase;
575
  }
576
  .model-badge.q4b { background: rgba(255,200,80,0.15); color: #ffc850; border: 1px solid rgba(255,200,80,0.35); }
@@ -578,470 +628,167 @@ async def homepage(request: Request):
578
  .model-badge.qvl { background: rgba(255,150,50,0.15); color: #ff9632; border: 1px solid rgba(255,150,50,0.35); }
579
  .model-badge.lfm450 { background: rgba(78,205,196,0.15); color: var(--accent2); border: 1px solid rgba(78,205,196,0.3); }
580
  .model-badge.lfm16 { background: rgba(107,203,119,0.15); color: #6bcb77; border: 1px solid rgba(107,203,119,0.35); }
 
581
 
582
  .model-info-box {
583
- border-radius: 6px; padding: 9px;
584
- font-size: 10px; color: var(--muted); line-height: 1.55;
585
- flex-shrink: 0;
586
- }
587
-
588
- /* ══════════════════════════════════════════
589
- ZOOM CONTROLS β€” fixed bottom-right
590
- ══════════════════════════════════════════ */
591
- .zoom-bar {
592
- position: fixed;
593
- bottom: 24px; right: 24px;
594
- z-index: 3000;
595
- display: flex; align-items: center; gap: 0;
596
- background: rgba(19,19,26,0.92);
597
- border: 1px solid var(--node-border);
598
- border-radius: 10px;
599
- backdrop-filter: blur(14px);
600
- box-shadow: 0 8px 32px rgba(0,0,0,0.55);
601
- overflow: hidden;
602
- user-select: none;
603
- }
604
-
605
- .zoom-btn {
606
- width: 36px; height: 36px;
607
- display: flex; align-items: center; justify-content: center;
608
- background: transparent;
609
- border: none; cursor: pointer;
610
- color: var(--muted);
611
- font-size: 18px; font-weight: 700;
612
- transition: background 0.15s, color 0.15s;
613
- font-family: 'JetBrains Mono', monospace;
614
  flex-shrink: 0;
615
  }
616
- .zoom-btn:hover { background: rgba(124,106,247,0.15); color: var(--accent); }
617
- .zoom-btn:active { background: rgba(124,106,247,0.28); }
618
 
619
- .zoom-divider {
620
- width: 1px; height: 20px;
621
- background: var(--node-border);
622
- flex-shrink: 0;
623
- }
624
-
625
- .zoom-track {
626
- position: relative;
627
- width: 100px; height: 36px;
628
- display: flex; align-items: center;
629
- padding: 0 10px;
630
- cursor: pointer;
631
- }
632
- .zoom-track-bg {
633
- width: 100%; height: 3px;
634
- background: var(--node-border);
635
- border-radius: 3px;
636
- position: relative;
637
- }
638
- .zoom-track-fill {
639
- position: absolute; left: 0; top: 0; height: 100%;
640
- background: linear-gradient(90deg, var(--accent), var(--accent2));
641
- border-radius: 3px;
642
- transition: width 0.1s;
643
- }
644
- .zoom-thumb {
645
- position: absolute;
646
- width: 13px; height: 13px;
647
- background: #fff;
648
- border: 2px solid var(--accent);
649
- border-radius: 50%;
650
- top: 50%; transform: translateY(-50%);
651
- box-shadow: 0 0 6px rgba(124,106,247,0.5);
652
- transition: left 0.1s;
653
- pointer-events: none;
654
- }
655
-
656
- .zoom-label {
657
- min-width: 42px; height: 36px;
658
- display: flex; align-items: center; justify-content: center;
659
- font-size: 10px; font-weight: 700;
660
- color: var(--accent);
661
- font-family: 'JetBrains Mono', monospace;
662
- letter-spacing: 0.04em;
663
- border-left: 1px solid var(--node-border);
664
- padding: 0 8px;
665
- }
666
-
667
- .zoom-reset {
668
- height: 36px;
669
- display: flex; align-items: center; justify-content: center;
670
- background: transparent;
671
- border: none; border-left: 1px solid var(--node-border);
672
- cursor: pointer;
673
- color: var(--muted);
674
- font-size: 9px; font-weight: 700;
675
- font-family: 'JetBrains Mono', monospace;
676
- letter-spacing: 0.06em;
677
- padding: 0 10px;
678
- transition: background 0.15s, color 0.15s;
679
- text-transform: uppercase;
680
- }
681
- .zoom-reset:hover { background: rgba(78,205,196,0.12); color: var(--accent2); }
682
  </style>
683
  </head>
684
  <body>
685
 
686
- <!-- Top Bar -->
687
  <div class="top-bar">
688
  <span class="logo">MULTIMODAL EDGE</span>
689
  <span class="sep">|</span>
690
  <span class="sub">Node-Based Inference Canvas</span>
691
- <span class="badge">v2.3 β€” PENTA MODEL</span>
692
  </div>
693
 
694
- <!-- Viewport + World -->
695
- <div id="viewport">
696
- <div id="world">
697
- <svg class="wires">
698
- <path id="wire-img-task" class="wire" />
699
- <path id="wire-model-task" class="wire" />
700
- <path id="wire-task-out" class="wire" />
701
- <path id="wire-task-gnd" class="wire" />
702
- </svg>
703
-
704
- <!-- ─── ID 01 : Image Input ─── -->
705
- <div class="node fixed-height" id="node-img" style="left:60px; top:60px;">
706
- <div class="node-header">
707
- <span><span class="status-dot" id="dot-img"></span>Input Image</span>
708
- <span class="id">ID: 01</span>
709
- </div>
710
- <div class="node-body">
711
- <div>
712
- <label>Upload Image</label>
713
- <div class="file-upload" id="dropZone">
714
- <svg width="30" height="30" viewBox="0 0 24 24" fill="none"
715
- stroke="#7c6af7" stroke-width="1.5"
716
- stroke-linecap="round" stroke-linejoin="round">
717
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
718
- <circle cx="8.5" cy="8.5" r="1.5"/>
719
- <polyline points="21 15 16 10 5 21"/>
720
- </svg>
721
- <span>Click or drop image here</span>
722
- <input type="file" id="fileInput" accept="image/*">
723
- </div>
724
- <img id="imgPreview" class="img-preview" />
725
  </div>
 
726
  </div>
727
- <div class="port out" id="port-img-out" style="top:50%;transform:translateY(-50%);"></div>
728
  </div>
 
 
729
 
730
- <!-- ─── ID 02 : Model Selector ─── -->
731
- <div class="node fixed-height" id="node-model" style="left:60px; top:410px;">
732
- <div class="node-header">
733
- <span><span class="status-dot" id="dot-model"></span>Model Selector</span>
734
- <span class="id">ID: 02</span>
 
 
 
 
 
 
 
 
 
 
 
 
735
  </div>
736
- <div class="node-body">
737
- <div>
738
- <label>Active Model</label>
739
- <select id="modelSelect">
740
- <option value="qwen_4b">Qwen3.5-4B</option>
741
- <option value="qwen_2b">Qwen3.5-2B</option>
742
- <option value="qwen_vl">Qwen3-VL-2B-Instruct</option>
743
- <option value="lfm_450">LFM2.5-VL-450M (LiquidAI)</option>
744
- <option value="lfm_16">LFM2.5-VL-1.6B (LiquidAI)</option>
745
- </select>
746
- </div>
747
- <div id="modelInfoBox" class="model-info-box"
748
- style="background:rgba(255,200,80,0.07);border:1px solid rgba(255,200,80,0.3);">
749
- <span class="model-badge q4b">QWEN 3.5 Β· 4B</span><br><br>
750
- Qwen3.5 4B multimodal model by Alibaba Cloud.
751
- Enhanced capacity over 2B β€” richer reasoning &amp; better instruction following.
752
- </div>
753
- <div style="flex:1;"></div>
754
  </div>
755
- <div class="port out" id="port-model-out" style="top:50%;transform:translateY(-50%);"></div>
756
  </div>
 
 
757
 
758
- <!-- ─── ID 03 : Task Config ─── -->
759
- <div class="node fixed-height" id="node-task" style="left:450px; top:60px;">
760
- <div class="port in" id="port-task-in" style="top:50%;transform:translateY(-50%);"></div>
761
- <div class="node-header">
762
- <span><span class="status-dot" id="dot-task"></span>Task Config</span>
763
- <span class="id">ID: 03</span>
 
 
 
 
 
 
 
 
 
 
764
  </div>
765
- <div class="node-body">
766
- <div>
767
- <label>Task Category</label>
768
- <select id="categorySelect">
769
- <option value="Query">Query</option>
770
- <option value="Caption">Caption</option>
771
- <option value="Point">Point</option>
772
- <option value="Detect">Detect</option>
773
- </select>
774
- </div>
775
- <div>
776
- <label>Prompt Directive</label>
777
- <textarea id="promptInput" rows="4"
778
- placeholder="e.g., Count the total number of boats and describe the environment."></textarea>
779
- </div>
780
- <button class="run-btn" id="runBtn">
781
- <span>Execute</span>
782
- <span class="loader" id="btnLoader"></span>
783
- </button>
784
  </div>
785
- <div class="port out" id="port-task-out" style="top:50%;transform:translateY(-50%);"></div>
 
 
 
786
  </div>
 
 
787
 
788
- <!-- ─── ID 04 : Output Stream ─── -->
789
- <div class="node fixed-height" id="node-out" style="left:840px; top:60px;">
790
- <div class="port in" id="port-out-in" style="top:50%;transform:translateY(-50%);"></div>
791
- <div class="node-header">
792
- <span><span class="status-dot" id="dot-out"></span>Output Stream</span>
793
- <span class="id">ID: 04</span>
794
- </div>
795
- <div class="node-body">
796
- <label>Streamed Result</label>
797
- <div class="output-box" id="outputBox">Results will stream here...</div>
798
- </div>
799
  </div>
 
800
 
801
- <!-- ─── ID 05 : Grounding Visualiser ─── -->
802
- <div class="node fixed-height" id="node-gnd" style="left:840px; top:410px;">
803
- <div class="port in" id="port-gnd-in" style="top:50%;transform:translateY(-50%);"></div>
804
- <div class="node-header">
805
- <span><span class="status-dot" id="dot-gnd"></span>View Grounding</span>
806
- <span class="id">ID: 05</span>
807
- </div>
808
- <div class="node-body">
809
- <label>Point / Detect Overlay</label>
810
- <div class="ground-canvas-wrap">
811
- <canvas id="groundCanvas"></canvas>
812
- <div class="ground-placeholder" id="groundPlaceholder">
813
- Active for Point / Detect tasks.<br>Run inference to visualise.
814
- </div>
815
  </div>
816
  </div>
817
  </div>
818
-
819
- </div><!-- /#world -->
820
- </div><!-- /#viewport -->
821
-
822
- <!-- ══ ZOOM BAR ══ -->
823
- <div class="zoom-bar" id="zoomBar">
824
- <!-- Zoom out -->
825
- <button class="zoom-btn" id="zoomOut" title="Zoom Out">
826
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none"
827
- stroke="currentColor" stroke-width="2" stroke-linecap="round">
828
- <line x1="2" y1="7" x2="12" y2="7"/>
829
- </svg>
830
- </button>
831
-
832
- <div class="zoom-divider"></div>
833
-
834
- <!-- Slider track -->
835
- <div class="zoom-track" id="zoomTrack">
836
- <div class="zoom-track-bg">
837
- <div class="zoom-track-fill" id="zoomFill"></div>
838
- </div>
839
- <div class="zoom-thumb" id="zoomThumb"></div>
840
  </div>
841
 
842
- <div class="zoom-divider"></div>
843
-
844
- <!-- Zoom in -->
845
- <button class="zoom-btn" id="zoomIn" title="Zoom In">
846
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none"
847
- stroke="currentColor" stroke-width="2" stroke-linecap="round">
848
- <line x1="7" y1="2" x2="7" y2="12"/>
849
- <line x1="2" y1="7" x2="12" y2="7"/>
850
- </svg>
851
- </button>
852
-
853
- <div class="zoom-divider"></div>
854
-
855
- <!-- Percentage label -->
856
- <div class="zoom-label" id="zoomLabel">100%</div>
857
-
858
- <!-- Reset -->
859
- <button class="zoom-reset" id="zoomReset" title="Reset Zoom">FIT</button>
860
  </div>
861
 
862
  <script>
863
- // ══════════════════════════════════════════════════════════
864
- // PAN + ZOOM ENGINE
865
- // ════════════════════════════���═════════════════════════════
866
- const viewport = document.getElementById('viewport');
867
- const world = document.getElementById('world');
868
-
869
- const ZOOM_MIN = 0.25;
870
- const ZOOM_MAX = 2.0;
871
- const ZOOM_STEP = 0.1;
872
-
873
- let scale = 1.0;
874
- let panX = 0;
875
- let panY = 0;
876
- let isPanning = false;
877
- let panStartX, panStartY, panOriginX, panOriginY;
878
-
879
- function clamp(v, lo, hi) { return Math.min(Math.max(v, lo), hi); }
880
-
881
- function applyTransform() {
882
- world.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
883
- updateZoomUI();
884
- updateWires();
885
- }
886
-
887
- // Centre the canvas initially
888
- function fitToView() {
889
- const vw = viewport.clientWidth;
890
- const vh = viewport.clientHeight;
891
- scale = clamp(Math.min(vw / 1360, vh / 800), ZOOM_MIN, ZOOM_MAX);
892
- panX = (vw - 1360 * scale) / 2;
893
- panY = (vh - 800 * scale) / 2;
894
- applyTransform();
895
- }
896
-
897
- // ── Zoom toward a point ──
898
- function zoomAt(newScale, cx, cy) {
899
- newScale = clamp(newScale, ZOOM_MIN, ZOOM_MAX);
900
- const ratio = newScale / scale;
901
- panX = cx - ratio * (cx - panX);
902
- panY = cy - ratio * (cy - panY);
903
- scale = newScale;
904
- applyTransform();
905
- }
906
-
907
- // ── Mouse wheel ──
908
- viewport.addEventListener('wheel', e => {
909
- e.preventDefault();
910
- const rect = viewport.getBoundingClientRect();
911
- const cx = e.clientX - rect.left;
912
- const cy = e.clientY - rect.top;
913
- const delta = e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP;
914
- zoomAt(scale + delta, cx, cy);
915
- }, { passive: false });
916
-
917
- // ── Middle-mouse / space-drag pan ──
918
- let spaceDown = false;
919
- document.addEventListener('keydown', e => { if (e.code === 'Space') { spaceDown = true; viewport.style.cursor = 'grab'; } });
920
- document.addEventListener('keyup', e => { if (e.code === 'Space') { spaceDown = false; viewport.style.cursor = 'default'; } });
921
-
922
- viewport.addEventListener('mousedown', e => {
923
- if (e.button === 1 || spaceDown) {
924
- e.preventDefault();
925
- isPanning = true;
926
- panStartX = e.clientX; panStartY = e.clientY;
927
- panOriginX = panX; panOriginY = panY;
928
- viewport.style.cursor = 'grabbing';
929
- }
930
- });
931
- window.addEventListener('mousemove', e => {
932
- if (!isPanning) return;
933
- panX = panOriginX + (e.clientX - panStartX);
934
- panY = panOriginY + (e.clientY - panStartY);
935
- applyTransform();
936
- });
937
- window.addEventListener('mouseup', e => {
938
- if (isPanning) {
939
- isPanning = false;
940
- viewport.style.cursor = spaceDown ? 'grab' : 'default';
941
- }
942
- });
943
-
944
- // ── Touch pan / pinch ──
945
- let lastTouches = null;
946
- viewport.addEventListener('touchstart', e => {
947
- lastTouches = e.touches;
948
- }, { passive: true });
949
- viewport.addEventListener('touchmove', e => {
950
- e.preventDefault();
951
- if (e.touches.length === 1 && lastTouches?.length === 1) {
952
- const dx = e.touches[0].clientX - lastTouches[0].clientX;
953
- const dy = e.touches[0].clientY - lastTouches[0].clientY;
954
- panX += dx; panY += dy;
955
- applyTransform();
956
- } else if (e.touches.length === 2 && lastTouches?.length === 2) {
957
- const d0 = Math.hypot(lastTouches[0].clientX - lastTouches[1].clientX,
958
- lastTouches[0].clientY - lastTouches[1].clientY);
959
- const d1 = Math.hypot(e.touches[0].clientX - e.touches[1].clientX,
960
- e.touches[0].clientY - e.touches[1].clientY);
961
- const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
962
- const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
963
- const rect = viewport.getBoundingClientRect();
964
- zoomAt(scale * (d1 / d0), midX - rect.left, midY - rect.top);
965
- }
966
- lastTouches = e.touches;
967
- }, { passive: false });
968
-
969
- // ══════════════════════════════════════════════════════════
970
- // ZOOM BAR UI
971
- // ══════════════════════════════════════════════════════════
972
- const zoomInBtn = document.getElementById('zoomIn');
973
- const zoomOutBtn = document.getElementById('zoomOut');
974
- const zoomReset = document.getElementById('zoomReset');
975
- const zoomLabel = document.getElementById('zoomLabel');
976
- const zoomFill = document.getElementById('zoomFill');
977
- const zoomThumb = document.getElementById('zoomThumb');
978
- const zoomTrack = document.getElementById('zoomTrack');
979
-
980
- function updateZoomUI() {
981
- const pct = Math.round(scale * 100);
982
- zoomLabel.textContent = `${pct}%`;
983
-
984
- // Map scale [ZOOM_MIN … ZOOM_MAX] β†’ [0 … 100]
985
- const t = (scale - ZOOM_MIN) / (ZOOM_MAX - ZOOM_MIN);
986
- const trackW = zoomTrack.getBoundingClientRect().width || 100;
987
- const thumbPx = t * (trackW - 20) + 10; // 10px padding each side
988
-
989
- zoomFill.style.width = `${t * 100}%`;
990
- zoomThumb.style.left = `${thumbPx}px`;
991
- }
992
-
993
- // Zoom in / out buttons
994
- zoomInBtn.addEventListener('click', () => {
995
- const cx = viewport.clientWidth / 2;
996
- const cy = viewport.clientHeight / 2;
997
- zoomAt(scale + ZOOM_STEP, cx, cy);
998
- });
999
- zoomOutBtn.addEventListener('click', () => {
1000
- const cx = viewport.clientWidth / 2;
1001
- const cy = viewport.clientHeight / 2;
1002
- zoomAt(scale - ZOOM_STEP, cx, cy);
1003
- });
1004
-
1005
- // Reset / fit
1006
- zoomReset.addEventListener('click', fitToView);
1007
-
1008
- // Drag on the slider track
1009
- let sliderDragging = false;
1010
- zoomTrack.addEventListener('mousedown', e => {
1011
- sliderDragging = true;
1012
- handleSliderMove(e);
1013
- e.stopPropagation();
1014
- });
1015
- window.addEventListener('mousemove', e => {
1016
- if (!sliderDragging) return;
1017
- handleSliderMove(e);
1018
- });
1019
- window.addEventListener('mouseup', () => { sliderDragging = false; });
1020
-
1021
- function handleSliderMove(e) {
1022
- const rect = zoomTrack.getBoundingClientRect();
1023
- const t = clamp((e.clientX - rect.left - 10) / (rect.width - 20), 0, 1);
1024
- const newScale = ZOOM_MIN + t * (ZOOM_MAX - ZOOM_MIN);
1025
- const cx = viewport.clientWidth / 2;
1026
- const cy = viewport.clientHeight / 2;
1027
- zoomAt(newScale, cx, cy);
1028
- }
1029
-
1030
  // ══════════════════════════════════════════════
1031
- // WIRE DRAWING (world-relative)
1032
  // ══════════════════════════════════════════════
 
 
1033
  function portCenter(id) {
1034
  const el = document.getElementById(id);
1035
  if (!el) return { x: 0, y: 0 };
1036
- // Get position relative to #world (un-transformed)
1037
- let ox = 0, oy = 0, cur = el;
1038
- while (cur && cur !== world) {
1039
- ox += cur.offsetLeft; oy += cur.offsetTop;
1040
- cur = cur.offsetParent;
1041
- }
1042
  return {
1043
- x: ox + el.offsetWidth / 2,
1044
- y: oy + el.offsetHeight / 2,
1045
  };
1046
  }
1047
 
@@ -1051,51 +798,45 @@ function bezier(p1, p2) {
1051
  }
1052
 
1053
  function updateWires() {
1054
- const pairs = [
1055
  ['wire-img-task', 'port-img-out', 'port-task-in'],
1056
  ['wire-model-task', 'port-model-out', 'port-task-in'],
1057
  ['wire-task-out', 'port-task-out', 'port-out-in'],
1058
  ['wire-task-gnd', 'port-task-out', 'port-gnd-in'],
1059
  ];
1060
- for (const [id, from, to] of pairs) {
1061
  const el = document.getElementById(id);
1062
  if (el) el.setAttribute('d', bezier(portCenter(from), portCenter(to)));
1063
  }
1064
  }
1065
 
1066
  // ══════════════════════════════════════════════
1067
- // NODE DRAGGING
1068
  // ══════════════════════════════════════════════
1069
  document.querySelectorAll('.node').forEach(node => {
1070
  const header = node.querySelector('.node-header');
1071
- let dragging = false, sx, sy, il, it;
1072
-
1073
  header.addEventListener('mousedown', e => {
1074
- dragging = true;
1075
- sx = e.clientX; sy = e.clientY;
1076
  il = parseInt(node.style.left) || 0;
1077
  it = parseInt(node.style.top) || 0;
1078
- node.style.zIndex = 100;
1079
- e.preventDefault();
1080
- e.stopPropagation(); // don't trigger pan
1081
  });
1082
- window.addEventListener('mousemove', e => {
1083
- if (!dragging) return;
1084
- // Account for current scale when dragging
1085
- node.style.left = `${il + (e.clientX - sx) / scale}px`;
1086
- node.style.top = `${it + (e.clientY - sy) / scale}px`;
1087
  updateWires();
1088
  });
1089
- window.addEventListener('mouseup', () => {
1090
- if (dragging) { dragging = false; node.style.zIndex = 10; }
1091
  });
1092
  });
1093
 
1094
- // ══════════════════════════════════════════════
1095
- // INIT
1096
- // ══════════════════════════════════════════════
1097
- window.addEventListener('resize', () => { applyTransform(); });
1098
- requestAnimationFrame(() => { fitToView(); });
1099
 
1100
  // ══════════════════════════════════════════════
1101
  // FILE UPLOAD
@@ -1138,40 +879,52 @@ const MODEL_INFO = {
1138
  html: `<span class="model-badge q4b">QWEN 3.5 Β· 4B</span><br><br>
1139
  Qwen3.5 4B multimodal model by Alibaba Cloud.
1140
  Enhanced capacity over 2B β€” richer reasoning &amp; better instruction following.`,
1141
- bg: 'rgba(255,200,80,0.07)', border: 'rgba(255,200,80,0.30)',
 
1142
  },
1143
  qwen_2b: {
1144
  html: `<span class="model-badge q2b">QWEN 3.5 Β· 2B</span><br><br>
1145
  Qwen3.5 2B multimodal model by Alibaba Cloud.
1146
  Lightweight &amp; fast β€” ideal for quick Query, Caption, Point &amp; Detect tasks.`,
1147
- bg: 'rgba(124,106,247,0.07)', border: 'rgba(124,106,247,0.25)',
 
1148
  },
1149
  qwen_vl: {
1150
  html: `<span class="model-badge qvl">QWEN3-VL Β· 2B</span><br><br>
1151
  Qwen3-VL-2B-Instruct β€” dedicated vision-language model by Alibaba Cloud.
1152
  Strong spatial grounding, OCR &amp; instruction-following.`,
1153
- bg: 'rgba(255,150,50,0.07)', border: 'rgba(255,150,50,0.25)',
 
1154
  },
1155
  lfm_450: {
1156
  html: `<span class="model-badge lfm450">LFM Β· 450M</span><br><br>
1157
  LFM2.5-VL 450M by LiquidAI. Ultra-lightweight edge model
1158
  with solid grounding capabilities.`,
1159
- bg: 'rgba(78,205,196,0.07)', border: 'rgba(78,205,196,0.25)',
 
1160
  },
1161
  lfm_16: {
1162
  html: `<span class="model-badge lfm16">LFM Β· 1.6B</span><br><br>
1163
  LFM2.5-VL 1.6B by LiquidAI. Larger liquid-state model offering
1164
  enhanced reasoning &amp; richer visual understanding.`,
1165
- bg: 'rgba(107,203,119,0.07)', border: 'rgba(107,203,119,0.25)',
 
 
 
 
 
 
 
 
1166
  },
1167
  };
1168
 
1169
  modelSelect.onchange = () => {
1170
  const info = MODEL_INFO[modelSelect.value];
1171
  if (!info) return;
1172
- modelInfoBox.innerHTML = info.html;
1173
- modelInfoBox.style.background = info.bg;
1174
- modelInfoBox.style.border = `1px solid ${info.border}`;
1175
  };
1176
 
1177
  // ══════════════════════════════════════════════
@@ -1252,6 +1005,7 @@ function drawGrounding(imgSrc, jsonText) {
1252
  gCtx.font = `bold ${fs}px JetBrains Mono, monospace`;
1253
 
1254
  const items = Array.isArray(parsed) ? parsed : [parsed];
 
1255
  items.forEach((item, i) => {
1256
  const col = PALETTE[i % PALETTE.length];
1257
 
@@ -1264,11 +1018,17 @@ function drawGrounding(imgSrc, jsonText) {
1264
 
1265
  if (bbox) {
1266
  let [x1,y1,x2,y2] = bbox;
1267
- if (x1 <= 1 && y1 <= 1 && x2 <= 1 && y2 <= 1) { x1*=W; y1*=H; x2*=W; y2*=H; }
1268
- const bw = x2-x1, bh = y2-y1;
 
 
1269
  const lbl = item?.label || `${i+1}`;
1270
- gCtx.fillStyle = hexToRgba(col, 0.18); gCtx.fillRect(x1, y1, bw, bh);
1271
- gCtx.strokeStyle = col; gCtx.strokeRect(x1, y1, bw, bh);
 
 
 
 
1272
  const tw = gCtx.measureText(lbl).width;
1273
  const ph = fs*1.4, pw = tw+10;
1274
  const lx = x1, ly = Math.max(0, y1-ph);
@@ -1291,13 +1051,16 @@ function drawGrounding(imgSrc, jsonText) {
1291
  if (x <= 1 && y <= 1) { x*=W; y*=H; }
1292
  const r = Math.max(8, W/60);
1293
  const lbl = item?.label || `${i+1}`;
 
1294
  gCtx.beginPath();
1295
  gCtx.arc(x, y, r*1.6, 0, Math.PI*2);
1296
  gCtx.fillStyle = hexToRgba(col, 0.15); gCtx.fill();
 
1297
  gCtx.beginPath();
1298
  gCtx.arc(x, y, r, 0, Math.PI*2);
1299
- gCtx.fillStyle = col; gCtx.fill();
1300
  gCtx.strokeStyle = '#fff'; gCtx.stroke();
 
1301
  gCtx.fillStyle = '#fff';
1302
  gCtx.fillText(lbl, x+r+4, y+fs*0.4);
1303
  }
 
33
  else torch.float16
34
  )
35
 
36
+ QWEN_4B_MODEL_NAME = "Qwen/Qwen3.5-4B"
37
+ QWEN_2B_MODEL_NAME = "Qwen/Qwen3.5-2B"
38
+ QWEN_VL_MODEL_NAME = "Qwen/Qwen3-VL-2B-Instruct"
39
+ LFM_450_MODEL_NAME = "LiquidAI/LFM2.5-VL-450M"
40
+ LFM_16_MODEL_NAME = "LiquidAI/LFM2.5-VL-1.6B"
41
+ QWEN_UNREDACTED_NAME = "prithivMLmods/Qwen3.5-2B-Unredacted-MAX"
42
 
43
  # ── Qwen3.5-4B ──────────────────────────────────────────
44
  print(f"Loading Qwen3.5-4B model: {QWEN_4B_MODEL_NAME} on {DEVICE}...")
 
113
  lfm_16_model = None
114
  lfm_16_processor = None
115
 
116
+ # ── Qwen3.5-2B-Unredacted-MAX ───────────────────────────
117
+ print(f"Loading Qwen3.5-2B-Unredacted-MAX model: {QWEN_UNREDACTED_NAME} on {DEVICE}...")
118
+ try:
119
+ qwen_unredacted_model = Qwen3_5ForConditionalGeneration.from_pretrained(
120
+ QWEN_UNREDACTED_NAME, torch_dtype=DTYPE, device_map=DEVICE,
121
+ ).eval()
122
+ qwen_unredacted_processor = AutoProcessor.from_pretrained(QWEN_UNREDACTED_NAME)
123
+ print("Qwen3.5-2B-Unredacted-MAX model loaded successfully.")
124
+ except Exception as e:
125
+ print(f"Warning: Qwen3.5-2B-Unredacted-MAX model loading failed. Error: {e}")
126
+ qwen_unredacted_model = None
127
+ qwen_unredacted_processor = None
128
+
129
 
130
  # --- Utility Functions ---
131
  def safe_parse_json(text: str):
 
165
  yield f"data: {json.dumps({'chunk': '[Error] Qwen3.5-4B model not loaded.'})}\n\n"
166
  yield "data: [DONE]\n\n"
167
  return
168
+
169
  messages = [{"role": "user", "content": [
170
  {"type": "image", "image": image},
171
  {"type": "text", "text": full_prompt},
 
197
  yield f"data: {json.dumps({'chunk': '[Error] Qwen3.5-2B model not loaded.'})}\n\n"
198
  yield "data: [DONE]\n\n"
199
  return
200
+
201
  messages = [{"role": "user", "content": [
202
  {"type": "image", "image": image},
203
  {"type": "text", "text": full_prompt},
 
229
  yield f"data: {json.dumps({'chunk': '[Error] Qwen3-VL model not loaded.'})}\n\n"
230
  yield "data: [DONE]\n\n"
231
  return
232
+
233
  messages = [{"role": "user", "content": [
234
  {"type": "image", "image": image},
235
  {"type": "text", "text": full_prompt},
 
261
  yield f"data: {json.dumps({'chunk': '[Error] LFM-450M model not loaded.'})}\n\n"
262
  yield "data: [DONE]\n\n"
263
  return
264
+
265
  conversation = [{"role": "user", "content": [
266
  {"type": "image", "image": image},
267
  {"type": "text", "text": full_prompt},
 
290
  yield f"data: {json.dumps({'chunk': '[Error] LFM-1.6B model not loaded.'})}\n\n"
291
  yield "data: [DONE]\n\n"
292
  return
293
+
294
  conversation = [{"role": "user", "content": [
295
  {"type": "image", "image": image},
296
  {"type": "text", "text": full_prompt},
 
313
  yield f"data: {json.dumps({'chunk': tok})}\n\n"
314
  thread.join()
315
 
316
+ # ── Qwen3.5-2B-Unredacted-MAX ───────────────────────
317
+ elif model_id == "qwen_unredacted":
318
+ if qwen_unredacted_model is None or qwen_unredacted_processor is None:
319
+ yield f"data: {json.dumps({'chunk': '[Error] Qwen3.5-2B-Unredacted-MAX model not loaded.'})}\n\n"
320
+ yield "data: [DONE]\n\n"
321
+ return
322
+
323
+ messages = [{"role": "user", "content": [
324
+ {"type": "image", "image": image},
325
+ {"type": "text", "text": full_prompt},
326
+ ]}]
327
+ text_input = qwen_unredacted_processor.apply_chat_template(
328
+ messages, tokenize=False, add_generation_prompt=True
329
+ )
330
+ inputs = qwen_unredacted_processor(
331
+ text=[text_input], images=[image], return_tensors="pt", padding=True
332
+ ).to(qwen_unredacted_model.device)
333
+ streamer = TextIteratorStreamer(
334
+ qwen_unredacted_processor.tokenizer,
335
+ skip_prompt=True, skip_special_tokens=True, timeout=120,
336
+ )
337
+ thread = threading.Thread(
338
+ target=qwen_unredacted_model.generate,
339
+ kwargs=dict(**inputs, streamer=streamer, max_new_tokens=1024,
340
+ use_cache=True, temperature=1.5, min_p=0.1),
341
+ )
342
+ thread.start()
343
+ for tok in streamer:
344
+ if tok:
345
+ yield f"data: {json.dumps({'chunk': tok})}\n\n"
346
+ thread.join()
347
+
348
  yield "data: [DONE]\n\n"
349
 
350
 
 
399
  * { box-sizing: border-box; margin: 0; padding: 0; }
400
 
401
  html, body {
402
+ min-height: 100%;
 
403
  background: var(--bg);
404
  color: var(--text);
405
  font-family: 'JetBrains Mono', monospace;
406
  }
407
 
408
+ body {
409
+ background-image:
410
+ radial-gradient(circle at 20% 50%, rgba(124,106,247,0.04) 0%, transparent 50%),
411
+ radial-gradient(circle at 80% 20%, rgba(78,205,196,0.04) 0%, transparent 50%),
412
+ linear-gradient(var(--grid) 1px, transparent 1px),
413
+ linear-gradient(90deg, var(--grid) 1px, transparent 1px);
414
+ background-size: 100% 100%, 100% 100%, 28px 28px, 28px 28px;
415
+ overflow-x: auto;
416
+ overflow-y: auto;
417
+ }
418
+
419
  /* ── Top Bar ── */
420
  .top-bar {
421
+ position: sticky; top: 0; left: 0; right: 0;
422
+ height: 50px;
423
+ background: rgba(13,13,15,0.95);
424
  border-bottom: 1px solid var(--node-border);
425
+ display: flex; align-items: center; padding: 0 28px;
426
+ gap: 16px; z-index: 1000;
427
  backdrop-filter: blur(12px);
428
  }
429
+ .top-bar .logo { font-size: 15px; font-weight: 700; color: var(--accent); letter-spacing: 0.05em; }
430
+ .top-bar .sep { color: var(--node-border); font-size: 15px; }
431
+ .top-bar .sub { font-size: 13px; color: var(--muted); }
432
  .top-bar .badge {
433
  margin-left: auto;
434
  background: rgba(124,106,247,0.15);
435
  border: 1px solid rgba(124,106,247,0.3);
436
+ padding: 4px 14px; border-radius: 20px;
437
+ font-size: 11px; color: var(--accent);
438
  }
439
 
440
+ /* ── Canvas ── */
441
+ #canvas {
442
+ position: relative;
443
+ width: 1560px;
444
+ min-height: calc(100vh - 50px);
445
+ height: 1060px;
446
+ margin: 0 auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  }
448
 
449
  svg.wires {
 
466
  /* ── Nodes ── */
467
  .node {
468
  position: absolute;
469
+ width: 340px;
470
  background: var(--node-bg);
471
  border: 1px solid var(--node-border);
472
+ border-radius: 11px;
473
+ box-shadow: 0 10px 34px rgba(0,0,0,0.5);
474
  z-index: 10;
475
  display: flex; flex-direction: column;
476
  transition: box-shadow 0.2s;
477
  }
478
  .node:hover {
479
+ box-shadow: 0 10px 34px rgba(0,0,0,0.5), 0 0 0 1px rgba(124,106,247,0.3);
480
  }
481
+ .node.fixed-height { height: 390px; }
482
 
483
  .node-header {
484
  background: var(--node-header);
485
+ padding: 9px 14px;
486
  border-bottom: 1px solid var(--node-border);
487
+ border-radius: 11px 11px 0 0;
488
+ font-size: 13px; font-weight: 700;
489
  cursor: grab;
490
  display: flex; justify-content: space-between; align-items: center;
491
  flex-shrink: 0;
 
493
  }
494
  .node-header:active { cursor: grabbing; }
495
  .node-header .id {
496
+ font-size: 11px; color: var(--muted);
497
  background: rgba(255,255,255,0.04);
498
+ padding: 3px 9px; border-radius: 5px;
499
  }
500
 
501
  .node-body {
502
+ padding: 13px;
503
+ display: flex; flex-direction: column; gap: 10px;
504
  flex: 1; overflow: hidden;
505
  }
506
 
507
  /* ── Ports ── */
508
  .port {
509
  position: absolute;
510
+ width: 13px; height: 13px;
511
  background: var(--node-bg);
512
  border: 2px solid var(--port);
513
  border-radius: 50%; z-index: 30;
514
  }
515
+ .port.out { right: -7px; }
516
+ .port.in { left: -7px; }
517
 
518
  /* ── Labels ── */
519
  label {
520
+ font-size: 11px; color: var(--muted);
521
+ font-weight: 600; display: block; margin-bottom: 4px;
522
  letter-spacing: 0.07em; text-transform: uppercase;
523
  }
524
 
 
526
 
527
  .file-upload {
528
  border: 1.5px dashed var(--node-border);
529
+ border-radius: 8px; padding: 14px 12px;
530
  text-align: center; cursor: pointer;
531
+ font-size: 12px; color: var(--muted);
532
  transition: border-color 0.2s, background 0.2s;
533
  background: rgba(255,255,255,0.01);
534
+ display: flex; flex-direction: column; align-items: center; gap: 6px;
535
  }
536
  .file-upload:hover { border-color: var(--accent); background: rgba(124,106,247,0.04); }
537
  .file-upload svg { opacity: 0.5; transition: opacity 0.2s; }
538
  .file-upload:hover svg { opacity: 0.9; }
539
 
540
  .img-preview {
541
+ width: 100%; height: 200px;
542
+ object-fit: contain; border-radius: 6px;
543
  display: none; background: #000;
544
  border: 1px solid var(--node-border);
545
  }
 
548
  width: 100%;
549
  background: rgba(0,0,0,0.3);
550
  border: 1px solid var(--node-border);
551
+ color: var(--text); padding: 8px 11px;
552
+ border-radius: 6px; outline: none;
553
+ font-size: 12px; font-family: 'JetBrains Mono', monospace;
554
  resize: none; transition: border-color 0.2s;
555
  }
556
  select:focus, textarea:focus { border-color: var(--accent); }
 
559
  button.run-btn {
560
  background: linear-gradient(135deg, var(--accent), #9b59b6);
561
  color: #fff; border: none;
562
+ padding: 10px; border-radius: 7px;
563
+ font-weight: 700; font-size: 13px;
564
  font-family: 'JetBrains Mono', monospace;
565
  cursor: pointer;
566
  transition: opacity 0.2s, transform 0.1s;
567
+ display: flex; justify-content: center; align-items: center; gap: 10px;
568
  letter-spacing: 0.04em; flex-shrink: 0;
569
  }
570
  button.run-btn:hover { opacity: 0.9; }
 
574
  .output-box {
575
  background: rgba(0,0,0,0.4);
576
  border: 1px solid var(--node-border);
577
+ border-radius: 6px; padding: 12px;
578
  flex: 1; overflow-y: auto;
579
+ font-size: 12px; line-height: 1.65;
580
  color: #c8c8e0; white-space: pre-wrap;
581
  user-select: text;
582
  font-family: 'JetBrains Mono', monospace;
 
586
  .ground-canvas-wrap {
587
  position: relative; flex: 1;
588
  border: 1px solid var(--node-border);
589
+ border-radius: 6px; overflow: hidden;
590
  background: #000; min-height: 0;
591
  }
592
  .ground-canvas-wrap canvas {
 
596
  .ground-placeholder {
597
  position: absolute; inset: 0;
598
  display: flex; align-items: center; justify-content: center;
599
+ font-size: 12px; color: var(--muted); text-align: center; padding: 12px;
600
  }
601
 
602
  .loader {
603
+ width: 13px; height: 13px;
604
  border: 2px solid rgba(255,255,255,0.3);
605
  border-top-color: #fff; border-radius: 50%;
606
  animation: spin 0.7s linear infinite;
 
609
  @keyframes spin { to { transform: rotate(360deg); } }
610
 
611
  .status-dot {
612
+ width: 7px; height: 7px; border-radius: 50%;
613
+ background: var(--muted); display: inline-block; margin-right: 7px;
614
+ }
615
+ .status-dot.active {
616
+ background: var(--accent2);
617
+ box-shadow: 0 0 6px var(--accent2);
618
  }
 
619
 
620
  /* ── Model badges ── */
621
  .model-badge {
622
+ display: inline-block; padding: 3px 9px;
623
+ border-radius: 5px; font-size: 10px; font-weight: 700;
624
  letter-spacing: 0.06em; text-transform: uppercase;
625
  }
626
  .model-badge.q4b { background: rgba(255,200,80,0.15); color: #ffc850; border: 1px solid rgba(255,200,80,0.35); }
 
628
  .model-badge.qvl { background: rgba(255,150,50,0.15); color: #ff9632; border: 1px solid rgba(255,150,50,0.35); }
629
  .model-badge.lfm450 { background: rgba(78,205,196,0.15); color: var(--accent2); border: 1px solid rgba(78,205,196,0.3); }
630
  .model-badge.lfm16 { background: rgba(107,203,119,0.15); color: #6bcb77; border: 1px solid rgba(107,203,119,0.35); }
631
+ .model-badge.qunred { background: rgba(255,80,160,0.15); color: #ff50a0; border: 1px solid rgba(255,80,160,0.35); }
632
 
633
  .model-info-box {
634
+ border-radius: 7px; padding: 11px;
635
+ font-size: 11px; color: var(--muted); line-height: 1.6;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  flex-shrink: 0;
637
  }
 
 
638
 
639
+ .canvas-footer { height: 44px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
  </style>
641
  </head>
642
  <body>
643
 
 
644
  <div class="top-bar">
645
  <span class="logo">MULTIMODAL EDGE</span>
646
  <span class="sep">|</span>
647
  <span class="sub">Node-Based Inference Canvas</span>
648
+ <span class="badge">v2.4 β€” HEXA MODEL</span>
649
  </div>
650
 
651
+ <div id="canvas">
652
+ <svg class="wires">
653
+ <path id="wire-img-task" class="wire" />
654
+ <path id="wire-model-task" class="wire" />
655
+ <path id="wire-task-out" class="wire" />
656
+ <path id="wire-task-gnd" class="wire" />
657
+ </svg>
658
+
659
+ <!-- ─── ID 01 : Image Input ─── -->
660
+ <div class="node fixed-height" id="node-img" style="left:48px; top:60px;">
661
+ <div class="node-header">
662
+ <span><span class="status-dot" id="dot-img"></span>Input Image</span>
663
+ <span class="id">ID: 01</span>
664
+ </div>
665
+ <div class="node-body">
666
+ <div>
667
+ <label>Upload Image</label>
668
+ <div class="file-upload" id="dropZone">
669
+ <svg width="34" height="34" viewBox="0 0 24 24" fill="none"
670
+ stroke="#7c6af7" stroke-width="1.5"
671
+ stroke-linecap="round" stroke-linejoin="round">
672
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
673
+ <circle cx="8.5" cy="8.5" r="1.5"/>
674
+ <polyline points="21 15 16 10 5 21"/>
675
+ </svg>
676
+ <span>Click or drop image here</span>
677
+ <input type="file" id="fileInput" accept="image/*">
 
 
 
 
678
  </div>
679
+ <img id="imgPreview" class="img-preview" />
680
  </div>
 
681
  </div>
682
+ <div class="port out" id="port-img-out" style="top:50%;transform:translateY(-50%);"></div>
683
+ </div>
684
 
685
+ <!-- ─── ID 02 : Model Selector ─── -->
686
+ <div class="node fixed-height" id="node-model" style="left:48px; top:472px;">
687
+ <div class="node-header">
688
+ <span><span class="status-dot" id="dot-model"></span>Model Selector</span>
689
+ <span class="id">ID: 02</span>
690
+ </div>
691
+ <div class="node-body">
692
+ <div>
693
+ <label>Active Model</label>
694
+ <select id="modelSelect">
695
+ <option value="qwen_4b">Qwen3.5-4B</option>
696
+ <option value="qwen_2b">Qwen3.5-2B</option>
697
+ <option value="qwen_vl">Qwen3-VL-2B-Instruct</option>
698
+ <option value="lfm_450">LFM2.5-VL-450M (LiquidAI)</option>
699
+ <option value="lfm_16">LFM2.5-VL-1.6B (LiquidAI)</option>
700
+ <option value="qwen_unredacted">Qwen3.5-2B-Unredacted-MAX</option>
701
+ </select>
702
  </div>
703
+ <div id="modelInfoBox" class="model-info-box"
704
+ style="background:rgba(255,200,80,0.07);border:1px solid rgba(255,200,80,0.3);">
705
+ <span class="model-badge q4b">QWEN 3.5 Β· 4B</span><br><br>
706
+ Qwen3.5 4B multimodal model by Alibaba Cloud.
707
+ Enhanced capacity over 2B β€” richer reasoning, better instruction following.
 
 
 
 
 
 
 
 
 
 
 
 
 
708
  </div>
709
+ <div style="flex:1;"></div>
710
  </div>
711
+ <div class="port out" id="port-model-out" style="top:50%;transform:translateY(-50%);"></div>
712
+ </div>
713
 
714
+ <!-- ─── ID 03 : Task Config ─── -->
715
+ <div class="node fixed-height" id="node-task" style="left:490px; top:60px;">
716
+ <div class="port in" id="port-task-in" style="top:50%;transform:translateY(-50%);"></div>
717
+ <div class="node-header">
718
+ <span><span class="status-dot" id="dot-task"></span>Task Config</span>
719
+ <span class="id">ID: 03</span>
720
+ </div>
721
+ <div class="node-body">
722
+ <div>
723
+ <label>Task Category</label>
724
+ <select id="categorySelect">
725
+ <option value="Query">Query</option>
726
+ <option value="Caption">Caption</option>
727
+ <option value="Point">Point</option>
728
+ <option value="Detect">Detect</option>
729
+ </select>
730
  </div>
731
+ <div>
732
+ <label>Prompt Directive</label>
733
+ <textarea id="promptInput" rows="5"
734
+ placeholder="e.g., Count the total number of boats and describe the environment."></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  </div>
736
+ <button class="run-btn" id="runBtn">
737
+ <span>Execute</span>
738
+ <span class="loader" id="btnLoader"></span>
739
+ </button>
740
  </div>
741
+ <div class="port out" id="port-task-out" style="top:50%;transform:translateY(-50%);"></div>
742
+ </div>
743
 
744
+ <!-- ─── ID 04 : Output Stream ─── -->
745
+ <div class="node fixed-height" id="node-out" style="left:932px; top:60px;">
746
+ <div class="port in" id="port-out-in" style="top:50%;transform:translateY(-50%);"></div>
747
+ <div class="node-header">
748
+ <span><span class="status-dot" id="dot-out"></span>Output Stream</span>
749
+ <span class="id">ID: 04</span>
750
+ </div>
751
+ <div class="node-body">
752
+ <label>Streamed Result</label>
753
+ <div class="output-box" id="outputBox">Results will stream here...</div>
 
754
  </div>
755
+ </div>
756
 
757
+ <!-- ─── ID 05 : Grounding Visualiser ─── -->
758
+ <div class="node fixed-height" id="node-gnd" style="left:932px; top:472px;">
759
+ <div class="port in" id="port-gnd-in" style="top:50%;transform:translateY(-50%);"></div>
760
+ <div class="node-header">
761
+ <span><span class="status-dot" id="dot-gnd"></span>View Grounding</span>
762
+ <span class="id">ID: 05</span>
763
+ </div>
764
+ <div class="node-body">
765
+ <label>Point / Detect Overlay</label>
766
+ <div class="ground-canvas-wrap">
767
+ <canvas id="groundCanvas"></canvas>
768
+ <div class="ground-placeholder" id="groundPlaceholder">
769
+ Active for Point / Detect tasks.<br>Run inference to visualise.
 
770
  </div>
771
  </div>
772
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
  </div>
774
 
775
+ <div class="canvas-footer"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
776
  </div>
777
 
778
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
779
  // ══════════════════════════════════════════════
780
+ // WIRE DRAWING
781
  // ══════════════════════════════════════════════
782
+ const canvasEl = document.getElementById('canvas');
783
+
784
  function portCenter(id) {
785
  const el = document.getElementById(id);
786
  if (!el) return { x: 0, y: 0 };
787
+ const er = el.getBoundingClientRect();
788
+ const cr = canvasEl.getBoundingClientRect();
 
 
 
 
789
  return {
790
+ x: er.left + er.width / 2 - cr.left,
791
+ y: er.top + er.height / 2 - cr.top
792
  };
793
  }
794
 
 
798
  }
799
 
800
  function updateWires() {
801
+ const wires = [
802
  ['wire-img-task', 'port-img-out', 'port-task-in'],
803
  ['wire-model-task', 'port-model-out', 'port-task-in'],
804
  ['wire-task-out', 'port-task-out', 'port-out-in'],
805
  ['wire-task-gnd', 'port-task-out', 'port-gnd-in'],
806
  ];
807
+ for (const [id, from, to] of wires) {
808
  const el = document.getElementById(id);
809
  if (el) el.setAttribute('d', bezier(portCenter(from), portCenter(to)));
810
  }
811
  }
812
 
813
  // ══════════════════════════════════════════════
814
+ // DRAGGING
815
  // ══════════════════════════════════════════════
816
  document.querySelectorAll('.node').forEach(node => {
817
  const header = node.querySelector('.node-header');
818
+ let drag = false, sx, sy, il, it;
 
819
  header.addEventListener('mousedown', e => {
820
+ drag = true; sx = e.clientX; sy = e.clientY;
 
821
  il = parseInt(node.style.left) || 0;
822
  it = parseInt(node.style.top) || 0;
823
+ node.style.zIndex = 100; e.preventDefault();
 
 
824
  });
825
+ document.addEventListener('mousemove', e => {
826
+ if (!drag) return;
827
+ node.style.left = `${il + e.clientX - sx}px`;
828
+ node.style.top = `${it + e.clientY - sy}px`;
 
829
  updateWires();
830
  });
831
+ document.addEventListener('mouseup', () => {
832
+ if (drag) { drag = false; node.style.zIndex = 10; }
833
  });
834
  });
835
 
836
+ window.addEventListener('resize', updateWires);
837
+ window.addEventListener('scroll', updateWires);
838
+ document.addEventListener('scroll', updateWires, true);
839
+ requestAnimationFrame(updateWires);
 
840
 
841
  // ══════════════════════════════════════════════
842
  // FILE UPLOAD
 
879
  html: `<span class="model-badge q4b">QWEN 3.5 Β· 4B</span><br><br>
880
  Qwen3.5 4B multimodal model by Alibaba Cloud.
881
  Enhanced capacity over 2B β€” richer reasoning &amp; better instruction following.`,
882
+ bg: 'rgba(255,200,80,0.07)',
883
+ border: 'rgba(255,200,80,0.30)',
884
  },
885
  qwen_2b: {
886
  html: `<span class="model-badge q2b">QWEN 3.5 Β· 2B</span><br><br>
887
  Qwen3.5 2B multimodal model by Alibaba Cloud.
888
  Lightweight &amp; fast β€” ideal for quick Query, Caption, Point &amp; Detect tasks.`,
889
+ bg: 'rgba(124,106,247,0.07)',
890
+ border: 'rgba(124,106,247,0.25)',
891
  },
892
  qwen_vl: {
893
  html: `<span class="model-badge qvl">QWEN3-VL Β· 2B</span><br><br>
894
  Qwen3-VL-2B-Instruct β€” dedicated vision-language model by Alibaba Cloud.
895
  Strong spatial grounding, OCR &amp; instruction-following.`,
896
+ bg: 'rgba(255,150,50,0.07)',
897
+ border: 'rgba(255,150,50,0.25)',
898
  },
899
  lfm_450: {
900
  html: `<span class="model-badge lfm450">LFM Β· 450M</span><br><br>
901
  LFM2.5-VL 450M by LiquidAI. Ultra-lightweight edge model
902
  with solid grounding capabilities.`,
903
+ bg: 'rgba(78,205,196,0.07)',
904
+ border: 'rgba(78,205,196,0.25)',
905
  },
906
  lfm_16: {
907
  html: `<span class="model-badge lfm16">LFM Β· 1.6B</span><br><br>
908
  LFM2.5-VL 1.6B by LiquidAI. Larger liquid-state model offering
909
  enhanced reasoning &amp; richer visual understanding.`,
910
+ bg: 'rgba(107,203,119,0.07)',
911
+ border: 'rgba(107,203,119,0.25)',
912
+ },
913
+ qwen_unredacted: {
914
+ html: `<span class="model-badge qunred">QWEN 3.5 Β· 2B UNREDACTED MAX</span><br><br>
915
+ Qwen3.5-2B-Unredacted-MAX by prithivMLmods. Fine-tuned variant of Qwen3.5-2B
916
+ with uncensored &amp; extended instruction-following capabilities.`,
917
+ bg: 'rgba(255,80,160,0.07)',
918
+ border: 'rgba(255,80,160,0.25)',
919
  },
920
  };
921
 
922
  modelSelect.onchange = () => {
923
  const info = MODEL_INFO[modelSelect.value];
924
  if (!info) return;
925
+ modelInfoBox.innerHTML = info.html;
926
+ modelInfoBox.style.background = info.bg;
927
+ modelInfoBox.style.border = `1px solid ${info.border}`;
928
  };
929
 
930
  // ══════════════════════════════════════════════
 
1005
  gCtx.font = `bold ${fs}px JetBrains Mono, monospace`;
1006
 
1007
  const items = Array.isArray(parsed) ? parsed : [parsed];
1008
+
1009
  items.forEach((item, i) => {
1010
  const col = PALETTE[i % PALETTE.length];
1011
 
 
1018
 
1019
  if (bbox) {
1020
  let [x1,y1,x2,y2] = bbox;
1021
+ if (x1 <= 1 && y1 <= 1 && x2 <= 1 && y2 <= 1) {
1022
+ x1*=W; y1*=H; x2*=W; y2*=H;
1023
+ }
1024
+ const bw = x2-x1, bh = y2-y1;
1025
  const lbl = item?.label || `${i+1}`;
1026
+
1027
+ gCtx.fillStyle = hexToRgba(col, 0.18);
1028
+ gCtx.fillRect(x1, y1, bw, bh);
1029
+ gCtx.strokeStyle = col;
1030
+ gCtx.strokeRect(x1, y1, bw, bh);
1031
+
1032
  const tw = gCtx.measureText(lbl).width;
1033
  const ph = fs*1.4, pw = tw+10;
1034
  const lx = x1, ly = Math.max(0, y1-ph);
 
1051
  if (x <= 1 && y <= 1) { x*=W; y*=H; }
1052
  const r = Math.max(8, W/60);
1053
  const lbl = item?.label || `${i+1}`;
1054
+
1055
  gCtx.beginPath();
1056
  gCtx.arc(x, y, r*1.6, 0, Math.PI*2);
1057
  gCtx.fillStyle = hexToRgba(col, 0.15); gCtx.fill();
1058
+
1059
  gCtx.beginPath();
1060
  gCtx.arc(x, y, r, 0, Math.PI*2);
1061
+ gCtx.fillStyle = col; gCtx.fill();
1062
  gCtx.strokeStyle = '#fff'; gCtx.stroke();
1063
+
1064
  gCtx.fillStyle = '#fff';
1065
  gCtx.fillText(lbl, x+r+4, y+fs*0.4);
1066
  }