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

Update app.py

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