prithivMLmods commited on
Commit
b4e33a3
Β·
verified Β·
1 Parent(s): d0f220c

update app

Browse files
Files changed (1) hide show
  1. app.py +198 -166
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import os
3
  import io
4
  import json
@@ -468,30 +467,30 @@ async def homepage(request: Request):
468
  /* ── Top Bar ── */
469
  .top-bar {
470
  position: sticky; top: 0; left: 0; right: 0;
471
- height: 38px;
472
- background: rgba(13,13,15,0.96);
473
  border-bottom: 1px solid var(--node-border);
474
- display: flex; align-items: center; padding: 0 18px;
475
- gap: 10px; z-index: 1000;
476
  backdrop-filter: blur(12px);
477
  }
478
- .top-bar .logo { font-size: 12px; font-weight: 700; color: var(--accent); letter-spacing: 0.06em; }
479
- .top-bar .sep { color: var(--node-border); font-size: 12px; }
480
- .top-bar .sub { font-size: 10px; color: var(--muted); }
481
  .top-bar .badge {
482
  margin-left: auto;
483
- background: rgba(124,106,247,0.13);
484
- border: 1px solid rgba(124,106,247,0.28);
485
- padding: 2px 9px; border-radius: 20px;
486
- font-size: 9px; color: var(--accent); letter-spacing: 0.04em;
487
  }
488
 
489
  /* ── Canvas ── */
490
  #canvas {
491
  position: relative;
492
- width: 1340px;
493
- min-height: calc(100vh - 38px);
494
- height: 820px;
495
  margin: 0 auto;
496
  }
497
 
@@ -502,101 +501,100 @@ async def homepage(request: Request):
502
  overflow: visible;
503
  }
504
  path.wire {
505
- fill: none; stroke: var(--wire); stroke-width: 2;
506
  stroke-linecap: round;
507
  }
508
  path.wire.active {
509
- stroke: var(--wire-active); stroke-width: 2.5;
510
- stroke-dasharray: 7 3;
511
- animation: flow 0.55s linear infinite;
512
  }
513
- @keyframes flow { to { stroke-dashoffset: -20; } }
514
 
515
  /* ── Nodes ── */
516
  .node {
517
  position: absolute;
518
- width: 288px;
519
  background: var(--node-bg);
520
  border: 1px solid var(--node-border);
521
- border-radius: 8px;
522
- box-shadow: 0 4px 20px rgba(0,0,0,0.45);
523
  z-index: 10;
524
  display: flex; flex-direction: column;
525
  transition: box-shadow 0.2s;
526
  }
527
  .node:hover {
528
- box-shadow: 0 4px 20px rgba(0,0,0,0.45),
529
- 0 0 0 1px rgba(124,106,247,0.28);
530
  }
531
- .node.fixed-height { height: 292px; }
532
 
533
  .node-header {
534
  background: var(--node-header);
535
- padding: 5px 10px;
536
  border-bottom: 1px solid var(--node-border);
537
- border-radius: 8px 8px 0 0;
538
- font-size: 10px; font-weight: 700;
539
  cursor: grab;
540
  display: flex; justify-content: space-between; align-items: center;
541
  flex-shrink: 0;
542
  user-select: none;
543
- letter-spacing: 0.03em;
544
  }
545
  .node-header:active { cursor: grabbing; }
546
  .node-header .id {
547
- font-size: 9px; color: var(--muted);
548
  background: rgba(255,255,255,0.04);
549
- padding: 1px 6px; border-radius: 3px;
550
  }
551
 
552
  .node-body {
553
- padding: 8px;
554
- display: flex; flex-direction: column; gap: 6px;
555
  flex: 1; overflow: hidden;
556
  }
557
 
558
  /* ── Ports ── */
559
  .port {
560
  position: absolute;
561
- width: 10px; height: 10px;
562
  background: var(--node-bg);
563
  border: 2px solid var(--port);
564
  border-radius: 50%; z-index: 30;
565
  }
566
- .port.out { right: -5px; }
567
- .port.in { left: -5px; }
568
 
569
  /* ── Labels ── */
570
  label {
571
- font-size: 9px; color: var(--muted);
572
- font-weight: 600; display: block; margin-bottom: 2px;
573
- letter-spacing: 0.08em; text-transform: uppercase;
574
  }
575
 
576
  input[type="file"] { display: none; }
577
 
578
  /* ── Upload Zone ── */
579
  .file-upload {
580
- border: 1px dashed var(--node-border);
581
- border-radius: 6px; padding: 10px 8px;
582
  text-align: center; cursor: pointer;
583
- font-size: 10px; color: var(--muted);
584
  transition: border-color 0.2s, background 0.2s;
585
  background: rgba(255,255,255,0.01);
586
- display: flex; flex-direction: column; align-items: center; gap: 4px;
587
  }
588
  .file-upload:hover {
589
  border-color: var(--accent);
590
  background: rgba(124,106,247,0.04);
591
  }
592
- .file-upload svg { opacity: 0.45; transition: opacity 0.2s; }
593
- .file-upload:hover svg { opacity: 0.85; }
594
 
595
  /* ── Preview wrapper ── */
596
  .preview-wrap {
597
  display: none;
598
  position: relative;
599
- border-radius: 6px;
600
  overflow: hidden;
601
  border: 1px solid var(--node-border);
602
  background: #000;
@@ -605,7 +603,7 @@ async def homepage(request: Request):
605
 
606
  .img-preview {
607
  width: 100%;
608
- height: 148px;
609
  object-fit: contain;
610
  display: block;
611
  }
@@ -613,34 +611,35 @@ async def homepage(request: Request):
613
  /* ── Clear button ── */
614
  .clear-btn {
615
  position: absolute;
616
- top: 5px; right: 5px;
617
- width: 20px; height: 20px;
618
  border-radius: 50%;
619
- background: rgba(13,13,15,0.78);
620
- border: 1px solid rgba(255,107,107,0.45);
621
  color: var(--accent3);
622
  cursor: pointer;
623
  display: flex; align-items: center; justify-content: center;
624
- transition: background 0.15s, transform 0.12s;
625
  z-index: 20;
626
- backdrop-filter: blur(4px);
627
  }
628
  .clear-btn:hover {
629
- background: rgba(255,107,107,0.20);
630
  border-color: var(--accent3);
631
- transform: scale(1.1);
632
  }
633
- .clear-btn:active { transform: scale(0.93); }
634
  .clear-btn svg { pointer-events: none; }
635
 
636
  /* ── Filename chip ── */
637
  .img-chip {
638
  display: none;
639
- align-items: center; gap: 5px;
640
- background: rgba(124,106,247,0.07);
641
- border: 1px solid rgba(124,106,247,0.18);
642
- border-radius: 4px;
643
- padding: 3px 7px;
 
644
  overflow: hidden;
645
  }
646
  .img-chip.visible { display: flex; }
@@ -654,48 +653,46 @@ async def homepage(request: Request):
654
  white-space: nowrap; flex: 1;
655
  color: var(--text); font-size: 9px;
656
  }
657
- .img-chip .chip-size { color: var(--muted); flex-shrink: 0; font-size: 9px; }
 
 
658
 
659
- /* ── Inputs ── */
660
  select, textarea {
661
  width: 100%;
662
- background: rgba(0,0,0,0.28);
663
  border: 1px solid var(--node-border);
664
- color: var(--text); padding: 5px 8px;
665
  border-radius: 5px; outline: none;
666
- font-size: 10px; font-family: 'JetBrains Mono', monospace;
667
  resize: none; transition: border-color 0.2s;
668
- line-height: 1.4;
669
  }
670
  select:focus, textarea:focus { border-color: var(--accent); }
671
  select option { background: #1c1c26; }
672
 
673
- /* ── Run Button ── */
674
  button.run-btn {
675
  background: linear-gradient(135deg, var(--accent), #9b59b6);
676
  color: #fff; border: none;
677
- padding: 6px 10px; border-radius: 5px;
678
- font-weight: 700; font-size: 10px;
679
  font-family: 'JetBrains Mono', monospace;
680
  cursor: pointer;
681
  transition: opacity 0.2s, transform 0.1s;
682
- display: flex; justify-content: center; align-items: center; gap: 6px;
683
- letter-spacing: 0.05em; flex-shrink: 0;
684
  }
685
- button.run-btn:hover { opacity: 0.88; }
686
- button.run-btn:active { transform: scale(0.97); }
687
  button.run-btn:disabled {
688
  background: var(--node-border); cursor: not-allowed; color: #555;
689
  }
690
 
691
- /* ── Output ── */
692
  .output-box {
693
- background: rgba(0,0,0,0.35);
694
  border: 1px solid var(--node-border);
695
- border-radius: 5px; padding: 8px;
696
  flex: 1; overflow-y: auto;
697
- font-size: 10px; line-height: 1.55;
698
- color: #c0c0da; white-space: pre-wrap;
699
  user-select: text;
700
  font-family: 'JetBrains Mono', monospace;
701
  }
@@ -714,53 +711,48 @@ async def homepage(request: Request):
714
  .ground-placeholder {
715
  position: absolute; inset: 0;
716
  display: flex; align-items: center; justify-content: center;
717
- font-size: 10px; color: var(--muted);
718
- text-align: center; padding: 8px; line-height: 1.5;
719
  }
720
 
721
- /* ── Model info box ── */
722
- .model-info-box {
723
- border-radius: 5px; padding: 7px 8px;
724
- font-size: 9px; color: var(--muted); line-height: 1.5;
725
- flex-shrink: 0;
726
- }
727
-
728
- /* ── Model badges ── */
729
- .model-badge {
730
- display: inline-block; padding: 1px 6px;
731
- border-radius: 3px; font-size: 8px; font-weight: 700;
732
- letter-spacing: 0.06em; text-transform: uppercase;
733
- }
734
- .model-badge.q4b { background: rgba(255,200,80,0.14); color: #ffc850; border: 1px solid rgba(255,200,80,0.32); }
735
- .model-badge.q2b { background: rgba(124,106,247,0.18); color: var(--accent); border: 1px solid rgba(124,106,247,0.28); }
736
- .model-badge.qvl { background: rgba(255,150,50,0.14); color: #ff9632; border: 1px solid rgba(255,150,50,0.32); }
737
- .model-badge.lfm450 { background: rgba(78,205,196,0.14); color: var(--accent2); border: 1px solid rgba(78,205,196,0.28); }
738
- .model-badge.lfm16 { background: rgba(107,203,119,0.14); color: #6bcb77; border: 1px solid rgba(107,203,119,0.32); }
739
- .model-badge.qunred { background: rgba(255,80,160,0.14); color: #ff50a0; border: 1px solid rgba(255,80,160,0.32); }
740
- .model-badge.q25vl3b { background: rgba(80,180,255,0.14); color: #50b4ff; border: 1px solid rgba(80,180,255,0.32); }
741
-
742
- /* ── Loader ── */
743
  .loader {
744
- width: 10px; height: 10px;
745
- border: 2px solid rgba(255,255,255,0.25);
746
  border-top-color: #fff; border-radius: 50%;
747
- animation: spin 0.65s linear infinite;
748
  display: none;
749
  }
750
  @keyframes spin { to { transform: rotate(360deg); } }
751
 
752
- /* ── Status dot ── */
753
  .status-dot {
754
- width: 5px; height: 5px; border-radius: 50%;
755
- background: var(--muted); display: inline-block; margin-right: 5px;
756
- flex-shrink: 0;
757
  }
758
  .status-dot.active {
759
  background: var(--accent2);
760
  box-shadow: 0 0 5px var(--accent2);
761
  }
762
 
763
- .canvas-footer { height: 28px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
  </style>
765
  </head>
766
  <body>
@@ -769,7 +761,7 @@ async def homepage(request: Request):
769
  <span class="logo">MULTIMODAL EDGE</span>
770
  <span class="sep">|</span>
771
  <span class="sub">Node-Based Inference Canvas</span>
772
- <span class="badge">v2.5 Β· HEPTA MODEL</span>
773
  </div>
774
 
775
  <div id="canvas">
@@ -781,37 +773,43 @@ async def homepage(request: Request):
781
  </svg>
782
 
783
  <!-- ─── ID 01 : Image Input ─── -->
784
- <div class="node fixed-height" id="node-img" style="left:38px; top:46px;">
785
  <div class="node-header">
786
  <span><span class="status-dot" id="dot-img"></span>Input Image</span>
787
- <span class="id">ID Β· 01</span>
788
  </div>
789
  <div class="node-body">
790
  <div>
791
  <label>Upload Image</label>
 
 
792
  <div class="file-upload" id="dropZone">
793
- <svg width="26" height="26" viewBox="0 0 24 24" fill="none"
794
  stroke="#7c6af7" stroke-width="1.5"
795
  stroke-linecap="round" stroke-linejoin="round">
796
- <rect x="3" y="3" width="18" height="18" rx="2"/>
797
  <circle cx="8.5" cy="8.5" r="1.5"/>
798
  <polyline points="21 15 16 10 5 21"/>
799
  </svg>
800
- <span>Click or drop image</span>
801
  <input type="file" id="fileInput" accept="image/*">
802
  </div>
 
 
803
  <div class="preview-wrap" id="previewWrap">
804
  <img id="imgPreview" class="img-preview" />
805
- <button class="clear-btn" id="clearBtn" title="Clear image">
806
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none"
807
- stroke="currentColor" stroke-width="2.8"
808
  stroke-linecap="round" stroke-linejoin="round">
809
  <line x1="18" y1="6" x2="6" y2="18"/>
810
  <line x1="6" y1="6" x2="18" y2="18"/>
811
  </svg>
812
  </button>
813
  </div>
814
- <div class="img-chip" id="imgChip" style="margin-top:5px;">
 
 
815
  <span class="chip-dot"></span>
816
  <span class="chip-name" id="chipName">β€”</span>
817
  <span class="chip-size" id="chipSize"></span>
@@ -822,10 +820,10 @@ async def homepage(request: Request):
822
  </div>
823
 
824
  <!-- ─── ID 02 : Model Selector ─── -->
825
- <div class="node fixed-height" id="node-model" style="left:38px; top:356px;">
826
  <div class="node-header">
827
  <span><span class="status-dot" id="dot-model"></span>Model Selector</span>
828
- <span class="id">ID Β· 02</span>
829
  </div>
830
  <div class="node-body">
831
  <div>
@@ -841,9 +839,10 @@ async def homepage(request: Request):
841
  </select>
842
  </div>
843
  <div id="modelInfoBox" class="model-info-box"
844
- style="background:rgba(255,200,80,0.06);border:1px solid rgba(255,200,80,0.28);">
845
  <span class="model-badge q4b">QWEN 3.5 Β· 4B</span><br><br>
846
- Qwen3.5-4B by Alibaba Cloud β€” enhanced reasoning &amp; instruction following.
 
847
  </div>
848
  <div style="flex:1;"></div>
849
  </div>
@@ -851,11 +850,11 @@ async def homepage(request: Request):
851
  </div>
852
 
853
  <!-- ─── ID 03 : Task Config ─── -->
854
- <div class="node fixed-height" id="node-task" style="left:416px; top:46px;">
855
  <div class="port in" id="port-task-in" style="top:50%;transform:translateY(-50%);"></div>
856
  <div class="node-header">
857
  <span><span class="status-dot" id="dot-task"></span>Task Config</span>
858
- <span class="id">ID Β· 03</span>
859
  </div>
860
  <div class="node-body">
861
  <div>
@@ -869,11 +868,11 @@ async def homepage(request: Request):
869
  </div>
870
  <div>
871
  <label>Prompt Directive</label>
872
- <textarea id="promptInput" rows="3"
873
- placeholder="e.g., Describe the scene in detail."></textarea>
874
  </div>
875
  <button class="run-btn" id="runBtn">
876
- <span>β–Ά Execute</span>
877
  <span class="loader" id="btnLoader"></span>
878
  </button>
879
  </div>
@@ -881,11 +880,11 @@ async def homepage(request: Request):
881
  </div>
882
 
883
  <!-- ─── ID 04 : Output Stream ─── -->
884
- <div class="node fixed-height" id="node-out" style="left:794px; top:46px;">
885
  <div class="port in" id="port-out-in" style="top:50%;transform:translateY(-50%);"></div>
886
  <div class="node-header">
887
  <span><span class="status-dot" id="dot-out"></span>Output Stream</span>
888
- <span class="id">ID Β· 04</span>
889
  </div>
890
  <div class="node-body">
891
  <label>Streamed Result</label>
@@ -894,11 +893,11 @@ async def homepage(request: Request):
894
  </div>
895
 
896
  <!-- ─── ID 05 : Grounding Visualiser ─── -->
897
- <div class="node fixed-height" id="node-gnd" style="left:794px; top:356px;">
898
  <div class="port in" id="port-gnd-in" style="top:50%;transform:translateY(-50%);"></div>
899
  <div class="node-header">
900
  <span><span class="status-dot" id="dot-gnd"></span>View Grounding</span>
901
- <span class="id">ID Β· 05</span>
902
  </div>
903
  <div class="node-body">
904
  <label>Point / Detect Overlay</label>
@@ -981,6 +980,7 @@ requestAnimationFrame(updateWires);
981
  // FILE UPLOAD + CLEAR
982
  // ══════════════════════════════════════════════
983
  let currentFile = null;
 
984
  const dropZone = document.getElementById('dropZone');
985
  const fileInput = document.getElementById('fileInput');
986
  const previewWrap = document.getElementById('previewWrap');
@@ -991,10 +991,10 @@ const chipName = document.getElementById('chipName');
991
  const chipSize = document.getElementById('chipSize');
992
  const dotImg = document.getElementById('dot-img');
993
 
994
- function formatBytes(b) {
995
- if (b < 1024) return b + ' B';
996
- if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
997
- return (b/1048576).toFixed(1) + ' MB';
998
  }
999
 
1000
  function handleFile(file) {
@@ -1043,32 +1043,53 @@ dotModel.classList.add('active');
1043
 
1044
  const MODEL_INFO = {
1045
  qwen_4b: {
1046
- html: `<span class="model-badge q4b">QWEN 3.5 Β· 4B</span><br><br>Qwen3.5-4B by Alibaba Cloud β€” enhanced reasoning &amp; instruction following.`,
1047
- bg: 'rgba(255,200,80,0.06)', border: 'rgba(255,200,80,0.28)',
 
 
 
1048
  },
1049
  qwen_2b: {
1050
- html: `<span class="model-badge q2b">QWEN 3.5 Β· 2B</span><br><br>Qwen3.5-2B by Alibaba Cloud β€” lightweight &amp; fast for all task types.`,
1051
- bg: 'rgba(124,106,247,0.06)', border: 'rgba(124,106,247,0.22)',
 
 
 
1052
  },
1053
  qwen_vl: {
1054
- html: `<span class="model-badge qvl">QWEN3-VL Β· 2B</span><br><br>Qwen3-VL-2B-Instruct β€” strong spatial grounding, OCR &amp; vision tasks.`,
1055
- bg: 'rgba(255,150,50,0.06)', border: 'rgba(255,150,50,0.22)',
 
 
 
1056
  },
1057
  lfm_450: {
1058
- html: `<span class="model-badge lfm450">LFM Β· 450M</span><br><br>LFM2.5-VL 450M by LiquidAI β€” ultra-lightweight edge grounding model.`,
1059
- bg: 'rgba(78,205,196,0.06)', border: 'rgba(78,205,196,0.22)',
 
 
 
1060
  },
1061
  lfm_16: {
1062
- html: `<span class="model-badge lfm16">LFM Β· 1.6B</span><br><br>LFM2.5-VL 1.6B by LiquidAI β€” richer reasoning &amp; visual understanding.`,
1063
- bg: 'rgba(107,203,119,0.06)', border: 'rgba(107,203,119,0.22)',
 
 
 
1064
  },
1065
  qwen_unredacted: {
1066
- html: `<span class="model-badge qunred">QWEN 3.5 Β· 2B UNREDACTED</span><br><br>Qwen3.5-2B-Unredacted-MAX by prithivMLmods β€” uncensored fine-tune.`,
1067
- bg: 'rgba(255,80,160,0.06)', border: 'rgba(255,80,160,0.22)',
 
 
 
1068
  },
1069
  qwen25_vl_3b: {
1070
- html: `<span class="model-badge q25vl3b">QWEN 2.5-VL Β· 3B</span><br><br>Qwen2.5-VL-3B-Instruct by Alibaba Cloud β€” powerful grounding &amp; OCR.`,
1071
- bg: 'rgba(80,180,255,0.06)', border: 'rgba(80,180,255,0.22)',
 
 
 
1072
  },
1073
  };
1074
 
@@ -1086,7 +1107,7 @@ modelSelect.onchange = () => {
1086
  const categorySelect = document.getElementById('categorySelect');
1087
  const promptInput = document.getElementById('promptInput');
1088
  const PLACEHOLDERS = {
1089
- Query: 'e.g., Count the boats and describe the scene.',
1090
  Caption: 'e.g., short | normal | detailed',
1091
  Point: 'e.g., The gun held by the person.',
1092
  Detect: 'e.g., The headlight of the car.',
@@ -1104,10 +1125,10 @@ function safeParseJSON(text) {
1104
  .replace(/\\s*```$/, '')
1105
  .trim();
1106
  try { return JSON.parse(text); } catch(_) {}
1107
- const am = text.match(/\\[[\\s\\S]*?\\]/);
1108
- if (am) { try { return JSON.parse(am[0]); } catch(_) {} }
1109
- const om = text.match(/\\{[\\s\\S]*?\\}/);
1110
- if (om) { try { return JSON.parse(om[0]); } catch(_) {} }
1111
  return null;
1112
  }
1113
 
@@ -1143,6 +1164,7 @@ function roundRect(ctx, x, y, w, h, r) {
1143
  function drawGrounding(imgSrc, jsonText) {
1144
  const parsed = safeParseJSON(jsonText);
1145
  if (!parsed) { console.warn('Grounding: could not parse JSON:', jsonText); return; }
 
1146
  const img = new Image();
1147
  img.onload = () => {
1148
  const W = img.naturalWidth, H = img.naturalHeight;
@@ -1157,9 +1179,11 @@ function drawGrounding(imgSrc, jsonText) {
1157
  gCtx.font = `bold ${fs}px JetBrains Mono, monospace`;
1158
 
1159
  const items = Array.isArray(parsed) ? parsed : [parsed];
 
1160
  items.forEach((item, i) => {
1161
  const col = PALETTE[i % PALETTE.length];
1162
 
 
1163
  let bbox = null;
1164
  if (item?.bbox_2d?.length === 4) bbox = item.bbox_2d;
1165
  else if (item?.bbox?.length === 4) bbox = item.bbox;
@@ -1168,13 +1192,17 @@ function drawGrounding(imgSrc, jsonText) {
1168
 
1169
  if (bbox) {
1170
  let [x1,y1,x2,y2] = bbox;
1171
- if (x1<=1&&y1<=1&&x2<=1&&y2<=1) { x1*=W; y1*=H; x2*=W; y2*=H; }
1172
- const bw = x2-x1, bh = y2-y1;
 
 
1173
  const lbl = item?.label || `${i+1}`;
1174
- gCtx.fillStyle = hexToRgba(col,0.18);
1175
- gCtx.fillRect(x1,y1,bw,bh);
 
1176
  gCtx.strokeStyle = col;
1177
- gCtx.strokeRect(x1,y1,bw,bh);
 
1178
  const tw = gCtx.measureText(lbl).width;
1179
  const ph = fs*1.4, pw = tw+10;
1180
  const lx = x1, ly = Math.max(0, y1-ph);
@@ -1185,6 +1213,7 @@ function drawGrounding(imgSrc, jsonText) {
1185
  return;
1186
  }
1187
 
 
1188
  let pt = null;
1189
  if (item?.point_2d?.length === 2) pt = item.point_2d;
1190
  else if (item?.point?.length === 2) pt = item.point;
@@ -1193,16 +1222,19 @@ function drawGrounding(imgSrc, jsonText) {
1193
 
1194
  if (pt) {
1195
  let [x,y] = pt;
1196
- if (x<=1&&y<=1) { x*=W; y*=H; }
1197
  const r = Math.max(8, W/60);
1198
  const lbl = item?.label || `${i+1}`;
 
1199
  gCtx.beginPath();
1200
  gCtx.arc(x, y, r*1.6, 0, Math.PI*2);
1201
- gCtx.fillStyle = hexToRgba(col,0.15); gCtx.fill();
 
1202
  gCtx.beginPath();
1203
  gCtx.arc(x, y, r, 0, Math.PI*2);
1204
  gCtx.fillStyle = col; gCtx.fill();
1205
  gCtx.strokeStyle = '#fff'; gCtx.stroke();
 
1206
  gCtx.fillStyle = '#fff';
1207
  gCtx.fillText(lbl, x+r+4, y+fs*0.4);
1208
  }
@@ -1301,4 +1333,4 @@ runBtn.onclick = async () => {
1301
  """
1302
 
1303
  if __name__ == "__main__":
1304
- app.launch(show_error=True)
 
 
1
  import os
2
  import io
3
  import json
 
467
  /* ── Top Bar ── */
468
  .top-bar {
469
  position: sticky; top: 0; left: 0; right: 0;
470
+ height: 42px;
471
+ background: rgba(13,13,15,0.95);
472
  border-bottom: 1px solid var(--node-border);
473
+ display: flex; align-items: center; padding: 0 20px;
474
+ gap: 12px; z-index: 1000;
475
  backdrop-filter: blur(12px);
476
  }
477
+ .top-bar .logo { font-size: 13px; font-weight: 700; color: var(--accent); letter-spacing: 0.05em; }
478
+ .top-bar .sep { color: var(--node-border); }
479
+ .top-bar .sub { font-size: 11px; color: var(--muted); }
480
  .top-bar .badge {
481
  margin-left: auto;
482
+ background: rgba(124,106,247,0.15);
483
+ border: 1px solid rgba(124,106,247,0.3);
484
+ padding: 3px 10px; border-radius: 20px;
485
+ font-size: 10px; color: var(--accent);
486
  }
487
 
488
  /* ── Canvas ── */
489
  #canvas {
490
  position: relative;
491
+ width: 1360px;
492
+ min-height: calc(100vh - 42px);
493
+ height: 900px;
494
  margin: 0 auto;
495
  }
496
 
 
501
  overflow: visible;
502
  }
503
  path.wire {
504
+ fill: none; stroke: var(--wire); stroke-width: 2.5;
505
  stroke-linecap: round;
506
  }
507
  path.wire.active {
508
+ stroke: var(--wire-active); stroke-width: 3;
509
+ stroke-dasharray: 8 4;
510
+ animation: flow 0.6s linear infinite;
511
  }
512
+ @keyframes flow { to { stroke-dashoffset: -24; } }
513
 
514
  /* ── Nodes ── */
515
  .node {
516
  position: absolute;
517
+ width: 295px;
518
  background: var(--node-bg);
519
  border: 1px solid var(--node-border);
520
+ border-radius: 9px;
521
+ box-shadow: 0 8px 28px rgba(0,0,0,0.5);
522
  z-index: 10;
523
  display: flex; flex-direction: column;
524
  transition: box-shadow 0.2s;
525
  }
526
  .node:hover {
527
+ box-shadow: 0 8px 28px rgba(0,0,0,0.5),
528
+ 0 0 0 1px rgba(124,106,247,0.3);
529
  }
530
+ .node.fixed-height { height: 340px; }
531
 
532
  .node-header {
533
  background: var(--node-header);
534
+ padding: 7px 12px;
535
  border-bottom: 1px solid var(--node-border);
536
+ border-radius: 9px 9px 0 0;
537
+ font-size: 11px; font-weight: 700;
538
  cursor: grab;
539
  display: flex; justify-content: space-between; align-items: center;
540
  flex-shrink: 0;
541
  user-select: none;
 
542
  }
543
  .node-header:active { cursor: grabbing; }
544
  .node-header .id {
545
+ font-size: 10px; color: var(--muted);
546
  background: rgba(255,255,255,0.04);
547
+ padding: 2px 7px; border-radius: 4px;
548
  }
549
 
550
  .node-body {
551
+ padding: 10px;
552
+ display: flex; flex-direction: column; gap: 8px;
553
  flex: 1; overflow: hidden;
554
  }
555
 
556
  /* ── Ports ── */
557
  .port {
558
  position: absolute;
559
+ width: 11px; height: 11px;
560
  background: var(--node-bg);
561
  border: 2px solid var(--port);
562
  border-radius: 50%; z-index: 30;
563
  }
564
+ .port.out { right: -6px; }
565
+ .port.in { left: -6px; }
566
 
567
  /* ── Labels ── */
568
  label {
569
+ font-size: 10px; color: var(--muted);
570
+ font-weight: 600; display: block; margin-bottom: 3px;
571
+ letter-spacing: 0.07em; text-transform: uppercase;
572
  }
573
 
574
  input[type="file"] { display: none; }
575
 
576
  /* ── Upload Zone ── */
577
  .file-upload {
578
+ border: 1.5px dashed var(--node-border);
579
+ border-radius: 7px; padding: 12px 10px;
580
  text-align: center; cursor: pointer;
581
+ font-size: 11px; color: var(--muted);
582
  transition: border-color 0.2s, background 0.2s;
583
  background: rgba(255,255,255,0.01);
584
+ display: flex; flex-direction: column; align-items: center; gap: 5px;
585
  }
586
  .file-upload:hover {
587
  border-color: var(--accent);
588
  background: rgba(124,106,247,0.04);
589
  }
590
+ .file-upload svg { opacity: 0.5; transition: opacity 0.2s; }
591
+ .file-upload:hover svg { opacity: 0.9; }
592
 
593
  /* ── Preview wrapper ── */
594
  .preview-wrap {
595
  display: none;
596
  position: relative;
597
+ border-radius: 7px;
598
  overflow: hidden;
599
  border: 1px solid var(--node-border);
600
  background: #000;
 
603
 
604
  .img-preview {
605
  width: 100%;
606
+ height: 170px;
607
  object-fit: contain;
608
  display: block;
609
  }
 
611
  /* ── Clear button ── */
612
  .clear-btn {
613
  position: absolute;
614
+ top: 6px; right: 6px;
615
+ width: 24px; height: 24px;
616
  border-radius: 50%;
617
+ background: rgba(13,13,15,0.80);
618
+ border: 1px solid var(--node-border);
619
  color: var(--accent3);
620
  cursor: pointer;
621
  display: flex; align-items: center; justify-content: center;
622
+ transition: background 0.18s, border-color 0.18s, transform 0.12s;
623
  z-index: 20;
624
+ backdrop-filter: blur(6px);
625
  }
626
  .clear-btn:hover {
627
+ background: rgba(255,107,107,0.18);
628
  border-color: var(--accent3);
629
+ transform: scale(1.08);
630
  }
631
+ .clear-btn:active { transform: scale(0.95); }
632
  .clear-btn svg { pointer-events: none; }
633
 
634
  /* ── Filename chip ── */
635
  .img-chip {
636
  display: none;
637
+ align-items: center; gap: 6px;
638
+ background: rgba(124,106,247,0.08);
639
+ border: 1px solid rgba(124,106,247,0.22);
640
+ border-radius: 5px;
641
+ padding: 4px 8px;
642
+ font-size: 9px; color: var(--muted);
643
  overflow: hidden;
644
  }
645
  .img-chip.visible { display: flex; }
 
653
  white-space: nowrap; flex: 1;
654
  color: var(--text); font-size: 9px;
655
  }
656
+ .img-chip .chip-size {
657
+ color: var(--muted); flex-shrink: 0; font-size: 9px;
658
+ }
659
 
 
660
  select, textarea {
661
  width: 100%;
662
+ background: rgba(0,0,0,0.3);
663
  border: 1px solid var(--node-border);
664
+ color: var(--text); padding: 7px 9px;
665
  border-radius: 5px; outline: none;
666
+ font-size: 11px; font-family: 'JetBrains Mono', monospace;
667
  resize: none; transition: border-color 0.2s;
 
668
  }
669
  select:focus, textarea:focus { border-color: var(--accent); }
670
  select option { background: #1c1c26; }
671
 
 
672
  button.run-btn {
673
  background: linear-gradient(135deg, var(--accent), #9b59b6);
674
  color: #fff; border: none;
675
+ padding: 8px; border-radius: 6px;
676
+ font-weight: 700; font-size: 11px;
677
  font-family: 'JetBrains Mono', monospace;
678
  cursor: pointer;
679
  transition: opacity 0.2s, transform 0.1s;
680
+ display: flex; justify-content: center; align-items: center; gap: 8px;
681
+ letter-spacing: 0.04em; flex-shrink: 0;
682
  }
683
+ button.run-btn:hover { opacity: 0.9; }
684
+ button.run-btn:active { transform: scale(0.98); }
685
  button.run-btn:disabled {
686
  background: var(--node-border); cursor: not-allowed; color: #555;
687
  }
688
 
 
689
  .output-box {
690
+ background: rgba(0,0,0,0.4);
691
  border: 1px solid var(--node-border);
692
+ border-radius: 5px; padding: 10px;
693
  flex: 1; overflow-y: auto;
694
+ font-size: 11px; line-height: 1.6;
695
+ color: #c8c8e0; white-space: pre-wrap;
696
  user-select: text;
697
  font-family: 'JetBrains Mono', monospace;
698
  }
 
711
  .ground-placeholder {
712
  position: absolute; inset: 0;
713
  display: flex; align-items: center; justify-content: center;
714
+ font-size: 11px; color: var(--muted); text-align: center; padding: 10px;
 
715
  }
716
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
  .loader {
718
+ width: 11px; height: 11px;
719
+ border: 2px solid rgba(255,255,255,0.3);
720
  border-top-color: #fff; border-radius: 50%;
721
+ animation: spin 0.7s linear infinite;
722
  display: none;
723
  }
724
  @keyframes spin { to { transform: rotate(360deg); } }
725
 
 
726
  .status-dot {
727
+ width: 6px; height: 6px; border-radius: 50%;
728
+ background: var(--muted); display: inline-block; margin-right: 6px;
 
729
  }
730
  .status-dot.active {
731
  background: var(--accent2);
732
  box-shadow: 0 0 5px var(--accent2);
733
  }
734
 
735
+ /* ── Model badges ── */
736
+ .model-badge {
737
+ display: inline-block; padding: 2px 7px;
738
+ border-radius: 4px; font-size: 9px; font-weight: 700;
739
+ letter-spacing: 0.06em; text-transform: uppercase;
740
+ }
741
+ .model-badge.q4b { background: rgba(255,200,80,0.15); color: #ffc850; border: 1px solid rgba(255,200,80,0.35); }
742
+ .model-badge.q2b { background: rgba(124,106,247,0.2); color: var(--accent); border: 1px solid rgba(124,106,247,0.3); }
743
+ .model-badge.qvl { background: rgba(255,150,50,0.15); color: #ff9632; border: 1px solid rgba(255,150,50,0.35); }
744
+ .model-badge.lfm450 { background: rgba(78,205,196,0.15); color: var(--accent2); border: 1px solid rgba(78,205,196,0.3); }
745
+ .model-badge.lfm16 { background: rgba(107,203,119,0.15); color: #6bcb77; border: 1px solid rgba(107,203,119,0.35); }
746
+ .model-badge.qunred { background: rgba(255,80,160,0.15); color: #ff50a0; border: 1px solid rgba(255,80,160,0.35); }
747
+ .model-badge.q25vl3b { background: rgba(80,180,255,0.15); color: #50b4ff; border: 1px solid rgba(80,180,255,0.35); }
748
+
749
+ .model-info-box {
750
+ border-radius: 6px; padding: 9px;
751
+ font-size: 10px; color: var(--muted); line-height: 1.55;
752
+ flex-shrink: 0;
753
+ }
754
+
755
+ .canvas-footer { height: 36px; }
756
  </style>
757
  </head>
758
  <body>
 
761
  <span class="logo">MULTIMODAL EDGE</span>
762
  <span class="sep">|</span>
763
  <span class="sub">Node-Based Inference Canvas</span>
764
+ <span class="badge">v2.5 β€” HEPTA MODEL</span>
765
  </div>
766
 
767
  <div id="canvas">
 
773
  </svg>
774
 
775
  <!-- ─── ID 01 : Image Input ─── -->
776
+ <div class="node fixed-height" id="node-img" style="left:40px; top:52px;">
777
  <div class="node-header">
778
  <span><span class="status-dot" id="dot-img"></span>Input Image</span>
779
+ <span class="id">ID: 01</span>
780
  </div>
781
  <div class="node-body">
782
  <div>
783
  <label>Upload Image</label>
784
+
785
+ <!-- Drop zone -->
786
  <div class="file-upload" id="dropZone">
787
+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none"
788
  stroke="#7c6af7" stroke-width="1.5"
789
  stroke-linecap="round" stroke-linejoin="round">
790
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
791
  <circle cx="8.5" cy="8.5" r="1.5"/>
792
  <polyline points="21 15 16 10 5 21"/>
793
  </svg>
794
+ <span>Click or drop image here</span>
795
  <input type="file" id="fileInput" accept="image/*">
796
  </div>
797
+
798
+ <!-- Preview -->
799
  <div class="preview-wrap" id="previewWrap">
800
  <img id="imgPreview" class="img-preview" />
801
+ <button class="clear-btn" id="clearBtn" title="Remove image">
802
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none"
803
+ stroke="currentColor" stroke-width="2.5"
804
  stroke-linecap="round" stroke-linejoin="round">
805
  <line x1="18" y1="6" x2="6" y2="18"/>
806
  <line x1="6" y1="6" x2="18" y2="18"/>
807
  </svg>
808
  </button>
809
  </div>
810
+
811
+ <!-- Filename chip -->
812
+ <div class="img-chip" id="imgChip" style="margin-top:6px;">
813
  <span class="chip-dot"></span>
814
  <span class="chip-name" id="chipName">β€”</span>
815
  <span class="chip-size" id="chipSize"></span>
 
820
  </div>
821
 
822
  <!-- ─── ID 02 : Model Selector ─── -->
823
+ <div class="node fixed-height" id="node-model" style="left:40px; top:412px;">
824
  <div class="node-header">
825
  <span><span class="status-dot" id="dot-model"></span>Model Selector</span>
826
+ <span class="id">ID: 02</span>
827
  </div>
828
  <div class="node-body">
829
  <div>
 
839
  </select>
840
  </div>
841
  <div id="modelInfoBox" class="model-info-box"
842
+ style="background:rgba(255,200,80,0.07);border:1px solid rgba(255,200,80,0.3);">
843
  <span class="model-badge q4b">QWEN 3.5 Β· 4B</span><br><br>
844
+ Qwen3.5 4B multimodal model by Alibaba Cloud.
845
+ Enhanced capacity over 2B β€” richer reasoning, better instruction following.
846
  </div>
847
  <div style="flex:1;"></div>
848
  </div>
 
850
  </div>
851
 
852
  <!-- ─── ID 03 : Task Config ─── -->
853
+ <div class="node fixed-height" id="node-task" style="left:425px; top:52px;">
854
  <div class="port in" id="port-task-in" style="top:50%;transform:translateY(-50%);"></div>
855
  <div class="node-header">
856
  <span><span class="status-dot" id="dot-task"></span>Task Config</span>
857
+ <span class="id">ID: 03</span>
858
  </div>
859
  <div class="node-body">
860
  <div>
 
868
  </div>
869
  <div>
870
  <label>Prompt Directive</label>
871
+ <textarea id="promptInput" rows="4"
872
+ placeholder="e.g., Count the total number of boats and describe the environment."></textarea>
873
  </div>
874
  <button class="run-btn" id="runBtn">
875
+ <span>Execute</span>
876
  <span class="loader" id="btnLoader"></span>
877
  </button>
878
  </div>
 
880
  </div>
881
 
882
  <!-- ─── ID 04 : Output Stream ─── -->
883
+ <div class="node fixed-height" id="node-out" style="left:810px; top:52px;">
884
  <div class="port in" id="port-out-in" style="top:50%;transform:translateY(-50%);"></div>
885
  <div class="node-header">
886
  <span><span class="status-dot" id="dot-out"></span>Output Stream</span>
887
+ <span class="id">ID: 04</span>
888
  </div>
889
  <div class="node-body">
890
  <label>Streamed Result</label>
 
893
  </div>
894
 
895
  <!-- ─── ID 05 : Grounding Visualiser ─── -->
896
+ <div class="node fixed-height" id="node-gnd" style="left:810px; top:412px;">
897
  <div class="port in" id="port-gnd-in" style="top:50%;transform:translateY(-50%);"></div>
898
  <div class="node-header">
899
  <span><span class="status-dot" id="dot-gnd"></span>View Grounding</span>
900
+ <span class="id">ID: 05</span>
901
  </div>
902
  <div class="node-body">
903
  <label>Point / Detect Overlay</label>
 
980
  // FILE UPLOAD + CLEAR
981
  // ══════════════════════════════════════════════
982
  let currentFile = null;
983
+
984
  const dropZone = document.getElementById('dropZone');
985
  const fileInput = document.getElementById('fileInput');
986
  const previewWrap = document.getElementById('previewWrap');
 
991
  const chipSize = document.getElementById('chipSize');
992
  const dotImg = document.getElementById('dot-img');
993
 
994
+ function formatBytes(bytes) {
995
+ if (bytes < 1024) return bytes + ' B';
996
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
997
+ return (bytes / 1048576).toFixed(1) + ' MB';
998
  }
999
 
1000
  function handleFile(file) {
 
1043
 
1044
  const MODEL_INFO = {
1045
  qwen_4b: {
1046
+ html: `<span class="model-badge q4b">QWEN 3.5 Β· 4B</span><br><br>
1047
+ Qwen3.5 4B multimodal model by Alibaba Cloud.
1048
+ Enhanced capacity over 2B β€” richer reasoning &amp; better instruction following.`,
1049
+ bg: 'rgba(255,200,80,0.07)',
1050
+ border: 'rgba(255,200,80,0.30)',
1051
  },
1052
  qwen_2b: {
1053
+ html: `<span class="model-badge q2b">QWEN 3.5 Β· 2B</span><br><br>
1054
+ Qwen3.5 2B multimodal model by Alibaba Cloud.
1055
+ Lightweight &amp; fast β€” ideal for quick Query, Caption, Point &amp; Detect tasks.`,
1056
+ bg: 'rgba(124,106,247,0.07)',
1057
+ border: 'rgba(124,106,247,0.25)',
1058
  },
1059
  qwen_vl: {
1060
+ html: `<span class="model-badge qvl">QWEN3-VL Β· 2B</span><br><br>
1061
+ Qwen3-VL-2B-Instruct β€” dedicated vision-language model by Alibaba Cloud.
1062
+ Strong spatial grounding, OCR &amp; instruction-following.`,
1063
+ bg: 'rgba(255,150,50,0.07)',
1064
+ border: 'rgba(255,150,50,0.25)',
1065
  },
1066
  lfm_450: {
1067
+ html: `<span class="model-badge lfm450">LFM Β· 450M</span><br><br>
1068
+ LFM2.5-VL 450M by LiquidAI. Ultra-lightweight edge model
1069
+ with solid grounding capabilities.`,
1070
+ bg: 'rgba(78,205,196,0.07)',
1071
+ border: 'rgba(78,205,196,0.25)',
1072
  },
1073
  lfm_16: {
1074
+ html: `<span class="model-badge lfm16">LFM Β· 1.6B</span><br><br>
1075
+ LFM2.5-VL 1.6B by LiquidAI. Larger liquid-state model offering
1076
+ enhanced reasoning &amp; richer visual understanding.`,
1077
+ bg: 'rgba(107,203,119,0.07)',
1078
+ border: 'rgba(107,203,119,0.25)',
1079
  },
1080
  qwen_unredacted: {
1081
+ html: `<span class="model-badge qunred">QWEN 3.5 Β· 2B UNREDACTED MAX</span><br><br>
1082
+ Qwen3.5-2B-Unredacted-MAX by prithivMLmods. Fine-tuned variant of Qwen3.5-2B
1083
+ with uncensored &amp; extended instruction-following capabilities.`,
1084
+ bg: 'rgba(255,80,160,0.07)',
1085
+ border: 'rgba(255,80,160,0.25)',
1086
  },
1087
  qwen25_vl_3b: {
1088
+ html: `<span class="model-badge q25vl3b">QWEN 2.5-VL Β· 3B</span><br><br>
1089
+ Qwen2.5-VL-3B-Instruct by Alibaba Cloud. Powerful 3B vision-language model
1090
+ with strong grounding, OCR &amp; multi-task visual reasoning.`,
1091
+ bg: 'rgba(80,180,255,0.07)',
1092
+ border: 'rgba(80,180,255,0.25)',
1093
  },
1094
  };
1095
 
 
1107
  const categorySelect = document.getElementById('categorySelect');
1108
  const promptInput = document.getElementById('promptInput');
1109
  const PLACEHOLDERS = {
1110
+ Query: 'e.g., Count the total number of boats and describe the environment.',
1111
  Caption: 'e.g., short | normal | detailed',
1112
  Point: 'e.g., The gun held by the person.',
1113
  Detect: 'e.g., The headlight of the car.',
 
1125
  .replace(/\\s*```$/, '')
1126
  .trim();
1127
  try { return JSON.parse(text); } catch(_) {}
1128
+ const arrMatch = text.match(/\\[[\\s\\S]*?\\]/);
1129
+ if (arrMatch) { try { return JSON.parse(arrMatch[0]); } catch(_) {} }
1130
+ const objMatch = text.match(/\\{[\\s\\S]*?\\}/);
1131
+ if (objMatch) { try { return JSON.parse(objMatch[0]); } catch(_) {} }
1132
  return null;
1133
  }
1134
 
 
1164
  function drawGrounding(imgSrc, jsonText) {
1165
  const parsed = safeParseJSON(jsonText);
1166
  if (!parsed) { console.warn('Grounding: could not parse JSON:', jsonText); return; }
1167
+
1168
  const img = new Image();
1169
  img.onload = () => {
1170
  const W = img.naturalWidth, H = img.naturalHeight;
 
1179
  gCtx.font = `bold ${fs}px JetBrains Mono, monospace`;
1180
 
1181
  const items = Array.isArray(parsed) ? parsed : [parsed];
1182
+
1183
  items.forEach((item, i) => {
1184
  const col = PALETTE[i % PALETTE.length];
1185
 
1186
+ // ── Bounding box ──
1187
  let bbox = null;
1188
  if (item?.bbox_2d?.length === 4) bbox = item.bbox_2d;
1189
  else if (item?.bbox?.length === 4) bbox = item.bbox;
 
1192
 
1193
  if (bbox) {
1194
  let [x1,y1,x2,y2] = bbox;
1195
+ if (x1 <= 1 && y1 <= 1 && x2 <= 1 && y2 <= 1) {
1196
+ x1*=W; y1*=H; x2*=W; y2*=H;
1197
+ }
1198
+ const bw = x2-x1, bh = y2-y1;
1199
  const lbl = item?.label || `${i+1}`;
1200
+
1201
+ gCtx.fillStyle = hexToRgba(col, 0.18);
1202
+ gCtx.fillRect(x1, y1, bw, bh);
1203
  gCtx.strokeStyle = col;
1204
+ gCtx.strokeRect(x1, y1, bw, bh);
1205
+
1206
  const tw = gCtx.measureText(lbl).width;
1207
  const ph = fs*1.4, pw = tw+10;
1208
  const lx = x1, ly = Math.max(0, y1-ph);
 
1213
  return;
1214
  }
1215
 
1216
+ // ── Point ──
1217
  let pt = null;
1218
  if (item?.point_2d?.length === 2) pt = item.point_2d;
1219
  else if (item?.point?.length === 2) pt = item.point;
 
1222
 
1223
  if (pt) {
1224
  let [x,y] = pt;
1225
+ if (x <= 1 && y <= 1) { x*=W; y*=H; }
1226
  const r = Math.max(8, W/60);
1227
  const lbl = item?.label || `${i+1}`;
1228
+
1229
  gCtx.beginPath();
1230
  gCtx.arc(x, y, r*1.6, 0, Math.PI*2);
1231
+ gCtx.fillStyle = hexToRgba(col, 0.15); gCtx.fill();
1232
+
1233
  gCtx.beginPath();
1234
  gCtx.arc(x, y, r, 0, Math.PI*2);
1235
  gCtx.fillStyle = col; gCtx.fill();
1236
  gCtx.strokeStyle = '#fff'; gCtx.stroke();
1237
+
1238
  gCtx.fillStyle = '#fff';
1239
  gCtx.fillText(lbl, x+r+4, y+fs*0.4);
1240
  }
 
1333
  """
1334
 
1335
  if __name__ == "__main__":
1336
+ app.launch(show_error=True)