prithivMLmods commited on
Commit
8fea3b1
Β·
verified Β·
1 Parent(s): 47078ff

update app

Browse files
Files changed (1) hide show
  1. app.py +230 -48
app.py CHANGED
@@ -788,6 +788,47 @@ async def homepage(request: Request):
788
  background: var(--node-border); cursor: not-allowed; color: #555;
789
  }
790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791
  .output-box {
792
  background: rgba(0,0,0,0.4);
793
  border: 1px solid var(--node-border);
@@ -797,6 +838,7 @@ async def homepage(request: Request):
797
  color: #c8c8e0; white-space: pre-wrap;
798
  user-select: text;
799
  font-family: 'JetBrains Mono', monospace;
 
800
  }
801
 
802
  /* ── Grounding ── */
@@ -986,8 +1028,19 @@ async def homepage(request: Request):
986
  <span><span class="status-dot" id="dot-out"></span>Output Stream</span>
987
  <span class="id">ID: 04</span>
988
  </div>
989
- <div class="node-body">
990
- <label>Streamed Result</label>
 
 
 
 
 
 
 
 
 
 
 
991
  <div class="output-box" id="outputBox">Results will stream here...</div>
992
  </div>
993
  </div>
@@ -1231,18 +1284,63 @@ categorySelect.onchange = e => {
1231
  };
1232
 
1233
  // ══════════════════════════════════════════════
1234
- // JSON PARSER
 
 
1235
  // ══════════════════════════════════════════════
1236
- function safeParseJSON(text) {
1237
- text = text.trim()
1238
- .replace(/^```(json)?\\s*/i, '')
1239
- .replace(/\\s*```$/, '')
1240
- .trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1241
  try { return JSON.parse(text); } catch(_) {}
1242
- const arrMatch = text.match(/\\[[\\s\\S]*?\\]/);
1243
- if (arrMatch) { try { return JSON.parse(arrMatch[0]); } catch(_) {} }
1244
- const objMatch = text.match(/\\{[\\s\\S]*?\\}/);
1245
- if (objMatch) { try { return JSON.parse(objMatch[0]); } catch(_) {} }
1246
  return null;
1247
  }
1248
 
@@ -1275,9 +1373,13 @@ function roundRect(ctx, x, y, w, h, r) {
1275
  ctx.closePath();
1276
  }
1277
 
1278
- function drawGrounding(imgSrc, jsonText) {
1279
- const parsed = safeParseJSON(jsonText);
1280
- if (!parsed) { console.warn('Grounding: could not parse JSON:', jsonText); return; }
 
 
 
 
1281
 
1282
  const img = new Image();
1283
  img.onload = () => {
@@ -1287,82 +1389,147 @@ function drawGrounding(imgSrc, jsonText) {
1287
  gCtx.drawImage(img, 0, 0);
1288
  groundPlaceholder.style.display = 'none';
1289
 
1290
- const lw = Math.max(2, W/200);
1291
- const fs = Math.max(12, W/40);
1292
  gCtx.lineWidth = lw;
1293
  gCtx.font = `bold ${fs}px JetBrains Mono, monospace`;
1294
 
 
1295
  const items = Array.isArray(parsed) ? parsed : [parsed];
1296
 
1297
  items.forEach((item, i) => {
1298
  const col = PALETTE[i % PALETTE.length];
1299
 
1300
- // ── Bounding box ──
 
1301
  let bbox = null;
1302
- if (item?.bbox_2d?.length === 4) bbox = item.bbox_2d;
1303
- else if (item?.bbox?.length === 4) bbox = item.bbox;
1304
- else if (Array.isArray(item) && item.length === 4 &&
1305
- item.every(n => typeof n === 'number')) bbox = item;
 
 
 
1306
 
1307
  if (bbox) {
1308
- let [x1,y1,x2,y2] = bbox;
 
 
1309
  if (x1 <= 1 && y1 <= 1 && x2 <= 1 && y2 <= 1) {
1310
- x1*=W; y1*=H; x2*=W; y2*=H;
1311
  }
1312
- const bw = x2-x1, bh = y2-y1;
1313
- const lbl = item?.label || `${i+1}`;
1314
 
 
 
 
 
 
1315
  gCtx.fillStyle = hexToRgba(col, 0.18);
1316
  gCtx.fillRect(x1, y1, bw, bh);
1317
  gCtx.strokeStyle = col;
1318
  gCtx.strokeRect(x1, y1, bw, bh);
1319
 
 
1320
  const tw = gCtx.measureText(lbl).width;
1321
- const ph = fs*1.4, pw = tw+10;
1322
- const lx = x1, ly = Math.max(0, y1-ph);
1323
  gCtx.fillStyle = col;
1324
- roundRect(gCtx, lx, ly, pw, ph, 4); gCtx.fill();
 
1325
  gCtx.fillStyle = '#fff';
1326
- gCtx.fillText(lbl, lx+5, ly+ph*0.76);
1327
  return;
1328
  }
1329
 
1330
- // ── Point ──
 
1331
  let pt = null;
1332
- if (item?.point_2d?.length === 2) pt = item.point_2d;
1333
- else if (item?.point?.length === 2) pt = item.point;
1334
- else if (Array.isArray(item) && item.length === 2 &&
1335
- item.every(n => typeof n === 'number')) pt = item;
 
 
 
1336
 
1337
  if (pt) {
1338
- let [x,y] = pt;
1339
- if (x <= 1 && y <= 1) { x*=W; y*=H; }
1340
- const r = Math.max(8, W/60);
1341
- const lbl = item?.label || `${i+1}`;
1342
 
 
 
 
 
1343
  gCtx.beginPath();
1344
- gCtx.arc(x, y, r*1.6, 0, Math.PI*2);
1345
- gCtx.fillStyle = hexToRgba(col, 0.15); gCtx.fill();
 
1346
 
 
1347
  gCtx.beginPath();
1348
- gCtx.arc(x, y, r, 0, Math.PI*2);
1349
- gCtx.fillStyle = col; gCtx.fill();
1350
- gCtx.strokeStyle = '#fff'; gCtx.stroke();
 
 
1351
 
 
1352
  gCtx.fillStyle = '#fff';
1353
- gCtx.fillText(lbl, x+r+4, y+fs*0.4);
1354
  }
1355
  });
1356
  };
1357
  img.src = imgSrc;
1358
  }
1359
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1360
  // ══════════════════════════════════════════════
1361
  // RUN INFERENCE
1362
  // ══════════════════════════════════════════════
1363
  const runBtn = document.getElementById('runBtn');
1364
  const btnLoader = document.getElementById('btnLoader');
1365
- const outputBox = document.getElementById('outputBox');
1366
  const allWires = ['wire-img-task','wire-model-task','wire-task-out','wire-task-gnd'];
1367
  const dotTask = document.getElementById('dot-task');
1368
  const dotOut = document.getElementById('dot-out');
@@ -1384,6 +1551,16 @@ runBtn.onclick = async () => {
1384
  dotGnd.classList.remove('active');
1385
  allWires.forEach(id => document.getElementById(id)?.classList.add('active'));
1386
 
 
 
 
 
 
 
 
 
 
 
1387
  const formData = new FormData();
1388
  formData.append('image', currentFile);
1389
  formData.append('category', categorySelect.value);
@@ -1425,10 +1602,15 @@ runBtn.onclick = async () => {
1425
  }
1426
 
1427
  dotOut.classList.add('active');
 
 
1428
  const cat = categorySelect.value;
1429
  if ((cat === 'Point' || cat === 'Detect') && fullText.trim()) {
1430
- dotGnd.classList.add('active');
1431
- drawGrounding(URL.createObjectURL(currentFile), fullText);
 
 
 
1432
  }
1433
 
1434
  } catch (err) {
 
788
  background: var(--node-border); cursor: not-allowed; color: #555;
789
  }
790
 
791
+ /* ── Output node body layout ── */
792
+ .output-node-body {
793
+ padding: 10px;
794
+ display: flex; flex-direction: column; gap: 6px;
795
+ flex: 1; overflow: hidden;
796
+ }
797
+
798
+ /* ── Output header row ── */
799
+ .output-header-row {
800
+ display: flex; align-items: center;
801
+ justify-content: space-between;
802
+ flex-shrink: 0;
803
+ }
804
+
805
+ /* ── Copy button ── */
806
+ .copy-btn {
807
+ display: flex; align-items: center; gap: 5px;
808
+ background: rgba(124,106,247,0.10);
809
+ border: 1px solid rgba(124,106,247,0.25);
810
+ border-radius: 5px;
811
+ padding: 3px 8px;
812
+ font-size: 9px; font-weight: 700;
813
+ font-family: 'JetBrains Mono', monospace;
814
+ color: var(--accent);
815
+ cursor: pointer;
816
+ letter-spacing: 0.05em;
817
+ transition: background 0.18s, border-color 0.18s, transform 0.1s;
818
+ flex-shrink: 0;
819
+ }
820
+ .copy-btn:hover {
821
+ background: rgba(124,106,247,0.22);
822
+ border-color: var(--accent);
823
+ }
824
+ .copy-btn:active { transform: scale(0.95); }
825
+ .copy-btn.copied {
826
+ background: rgba(78,205,196,0.15);
827
+ border-color: var(--accent2);
828
+ color: var(--accent2);
829
+ }
830
+ .copy-btn svg { pointer-events: none; flex-shrink: 0; }
831
+
832
  .output-box {
833
  background: rgba(0,0,0,0.4);
834
  border: 1px solid var(--node-border);
 
838
  color: #c8c8e0; white-space: pre-wrap;
839
  user-select: text;
840
  font-family: 'JetBrains Mono', monospace;
841
+ min-height: 0;
842
  }
843
 
844
  /* ── Grounding ── */
 
1028
  <span><span class="status-dot" id="dot-out"></span>Output Stream</span>
1029
  <span class="id">ID: 04</span>
1030
  </div>
1031
+ <div class="output-node-body">
1032
+ <div class="output-header-row">
1033
+ <label style="margin-bottom:0;">Streamed Result</label>
1034
+ <button class="copy-btn" id="copyBtn" title="Copy result to clipboard">
1035
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none"
1036
+ stroke="currentColor" stroke-width="2.2"
1037
+ stroke-linecap="round" stroke-linejoin="round">
1038
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
1039
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
1040
+ </svg>
1041
+ COPY
1042
+ </button>
1043
+ </div>
1044
  <div class="output-box" id="outputBox">Results will stream here...</div>
1045
  </div>
1046
  </div>
 
1284
  };
1285
 
1286
  // ══════════════════════════════════════════════
1287
+ // ROBUST JSON EXTRACTOR
1288
+ // Strips <think>…</think> blocks, then pulls
1289
+ // the first JSON array or object from the text.
1290
  // ══════════════════════════════════════════════
1291
+ function extractGroundingJSON(raw) {
1292
+ // 1. Remove <think>…</think> blocks (including nested content)
1293
+ let text = raw.replace(/<think>[\s\S]*?<\/think>/gi, '');
1294
+
1295
+ // 2. Strip markdown code fences ```json … ``` or ``` … ```
1296
+ text = text.replace(/```(?:json)?\\s*/gi, '').replace(/```/g, '');
1297
+
1298
+ text = text.trim();
1299
+
1300
+ // 3. Try to find a JSON array first [ … ]
1301
+ const arrIdx = text.indexOf('[');
1302
+ if (arrIdx !== -1) {
1303
+ // Walk forward to find the matching closing bracket
1304
+ let depth = 0, inStr = false, esc = false;
1305
+ for (let i = arrIdx; i < text.length; i++) {
1306
+ const c = text[i];
1307
+ if (esc) { esc = false; continue; }
1308
+ if (c === '\\\\') { esc = true; continue; }
1309
+ if (c === '"') { inStr = !inStr; continue; }
1310
+ if (inStr) continue;
1311
+ if (c === '[') depth++;
1312
+ if (c === ']') {
1313
+ depth--;
1314
+ if (depth === 0) {
1315
+ try { return JSON.parse(text.slice(arrIdx, i + 1)); } catch(_) { break; }
1316
+ }
1317
+ }
1318
+ }
1319
+ }
1320
+
1321
+ // 4. Try to find a JSON object { … }
1322
+ const objIdx = text.indexOf('{');
1323
+ if (objIdx !== -1) {
1324
+ let depth = 0, inStr = false, esc = false;
1325
+ for (let i = objIdx; i < text.length; i++) {
1326
+ const c = text[i];
1327
+ if (esc) { esc = false; continue; }
1328
+ if (c === '\\\\') { esc = true; continue; }
1329
+ if (c === '"') { inStr = !inStr; continue; }
1330
+ if (inStr) continue;
1331
+ if (c === '{') depth++;
1332
+ if (c === '}') {
1333
+ depth--;
1334
+ if (depth === 0) {
1335
+ try { return JSON.parse(text.slice(objIdx, i + 1)); } catch(_) { break; }
1336
+ }
1337
+ }
1338
+ }
1339
+ }
1340
+
1341
+ // 5. Last resort β€” try parsing the whole cleaned text
1342
  try { return JSON.parse(text); } catch(_) {}
1343
+
 
 
 
1344
  return null;
1345
  }
1346
 
 
1373
  ctx.closePath();
1374
  }
1375
 
1376
+ function drawGrounding(imgSrc, rawText) {
1377
+ // ── Extract JSON from raw model output (handles <think> blocks etc.) ──
1378
+ const parsed = extractGroundingJSON(rawText);
1379
+ if (!parsed) {
1380
+ console.warn('Grounding: could not extract JSON from output:', rawText);
1381
+ return;
1382
+ }
1383
 
1384
  const img = new Image();
1385
  img.onload = () => {
 
1389
  gCtx.drawImage(img, 0, 0);
1390
  groundPlaceholder.style.display = 'none';
1391
 
1392
+ const lw = Math.max(2, W / 200);
1393
+ const fs = Math.max(12, W / 40);
1394
  gCtx.lineWidth = lw;
1395
  gCtx.font = `bold ${fs}px JetBrains Mono, monospace`;
1396
 
1397
+ // Normalise to array
1398
  const items = Array.isArray(parsed) ? parsed : [parsed];
1399
 
1400
  items.forEach((item, i) => {
1401
  const col = PALETTE[i % PALETTE.length];
1402
 
1403
+ // ── Detect: bounding box ─────────────────────────
1404
+ // Accept bbox_2d, bbox, or a raw 4-number array
1405
  let bbox = null;
1406
+ if (Array.isArray(item?.bbox_2d) && item.bbox_2d.length === 4)
1407
+ bbox = item.bbox_2d;
1408
+ else if (Array.isArray(item?.bbox) && item.bbox.length === 4)
1409
+ bbox = item.bbox;
1410
+ else if (Array.isArray(item) && item.length === 4
1411
+ && item.every(n => typeof n === 'number'))
1412
+ bbox = item;
1413
 
1414
  if (bbox) {
1415
+ let [x1, y1, x2, y2] = bbox.map(Number);
1416
+
1417
+ // Normalised 0-1 coords β†’ pixel coords
1418
  if (x1 <= 1 && y1 <= 1 && x2 <= 1 && y2 <= 1) {
1419
+ x1 *= W; y1 *= H; x2 *= W; y2 *= H;
1420
  }
 
 
1421
 
1422
+ const bw = x2 - x1;
1423
+ const bh = y2 - y1;
1424
+ const lbl = item?.label ?? `obj ${i + 1}`;
1425
+
1426
+ // Filled rect + stroke
1427
  gCtx.fillStyle = hexToRgba(col, 0.18);
1428
  gCtx.fillRect(x1, y1, bw, bh);
1429
  gCtx.strokeStyle = col;
1430
  gCtx.strokeRect(x1, y1, bw, bh);
1431
 
1432
+ // Label pill above the box
1433
  const tw = gCtx.measureText(lbl).width;
1434
+ const ph = fs * 1.4, pw = tw + 10;
1435
+ const lx = x1, ly = Math.max(0, y1 - ph);
1436
  gCtx.fillStyle = col;
1437
+ roundRect(gCtx, lx, ly, pw, ph, 4);
1438
+ gCtx.fill();
1439
  gCtx.fillStyle = '#fff';
1440
+ gCtx.fillText(lbl, lx + 5, ly + ph * 0.76);
1441
  return;
1442
  }
1443
 
1444
+ // ── Point: 2-D coordinate ────────────────────────
1445
+ // Accept point_2d, point, or a raw 2-number array
1446
  let pt = null;
1447
+ if (Array.isArray(item?.point_2d) && item.point_2d.length === 2)
1448
+ pt = item.point_2d;
1449
+ else if (Array.isArray(item?.point) && item.point.length === 2)
1450
+ pt = item.point;
1451
+ else if (Array.isArray(item) && item.length === 2
1452
+ && item.every(n => typeof n === 'number'))
1453
+ pt = item;
1454
 
1455
  if (pt) {
1456
+ let [x, y] = pt.map(Number);
1457
+
1458
+ // Normalised 0-1 coords β†’ pixel coords
1459
+ if (x <= 1 && y <= 1) { x *= W; y *= H; }
1460
 
1461
+ const r = Math.max(8, W / 60);
1462
+ const lbl = item?.label ?? `pt ${i + 1}`;
1463
+
1464
+ // Outer glow ring
1465
  gCtx.beginPath();
1466
+ gCtx.arc(x, y, r * 1.7, 0, Math.PI * 2);
1467
+ gCtx.fillStyle = hexToRgba(col, 0.15);
1468
+ gCtx.fill();
1469
 
1470
+ // Solid dot
1471
  gCtx.beginPath();
1472
+ gCtx.arc(x, y, r, 0, Math.PI * 2);
1473
+ gCtx.fillStyle = col;
1474
+ gCtx.fill();
1475
+ gCtx.strokeStyle = '#fff';
1476
+ gCtx.stroke();
1477
 
1478
+ // Label to the right of the dot
1479
  gCtx.fillStyle = '#fff';
1480
+ gCtx.fillText(lbl, x + r + 4, y + fs * 0.4);
1481
  }
1482
  });
1483
  };
1484
  img.src = imgSrc;
1485
  }
1486
 
1487
+ // ══════════════════════════════════════════════
1488
+ // COPY BUTTON
1489
+ // ══════════════════════════════════════════════
1490
+ const copyBtn = document.getElementById('copyBtn');
1491
+ const outputBox = document.getElementById('outputBox');
1492
+ let copyTimer = null;
1493
+
1494
+ copyBtn.onclick = () => {
1495
+ const txt = outputBox.innerText || '';
1496
+ if (!txt || txt === 'Results will stream here...') return;
1497
+ navigator.clipboard.writeText(txt).then(() => {
1498
+ copyBtn.classList.add('copied');
1499
+ copyBtn.innerHTML = `
1500
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none"
1501
+ stroke="currentColor" stroke-width="2.5"
1502
+ stroke-linecap="round" stroke-linejoin="round">
1503
+ <polyline points="20 6 9 17 4 12"/>
1504
+ </svg> COPIED`;
1505
+ clearTimeout(copyTimer);
1506
+ copyTimer = setTimeout(() => {
1507
+ copyBtn.classList.remove('copied');
1508
+ copyBtn.innerHTML = `
1509
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none"
1510
+ stroke="currentColor" stroke-width="2.2"
1511
+ stroke-linecap="round" stroke-linejoin="round">
1512
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
1513
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
1514
+ </svg> COPY`;
1515
+ }, 2000);
1516
+ }).catch(() => {
1517
+ // Fallback for older browsers
1518
+ const ta = document.createElement('textarea');
1519
+ ta.value = txt;
1520
+ ta.style.position = 'fixed'; ta.style.opacity = '0';
1521
+ document.body.appendChild(ta);
1522
+ ta.select();
1523
+ document.execCommand('copy');
1524
+ document.body.removeChild(ta);
1525
+ });
1526
+ };
1527
+
1528
  // ══════════════════════════════════════════════
1529
  // RUN INFERENCE
1530
  // ══════════════════════════════════════════════
1531
  const runBtn = document.getElementById('runBtn');
1532
  const btnLoader = document.getElementById('btnLoader');
 
1533
  const allWires = ['wire-img-task','wire-model-task','wire-task-out','wire-task-gnd'];
1534
  const dotTask = document.getElementById('dot-task');
1535
  const dotOut = document.getElementById('dot-out');
 
1551
  dotGnd.classList.remove('active');
1552
  allWires.forEach(id => document.getElementById(id)?.classList.add('active'));
1553
 
1554
+ // Reset copy button
1555
+ copyBtn.classList.remove('copied');
1556
+ copyBtn.innerHTML = `
1557
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none"
1558
+ stroke="currentColor" stroke-width="2.2"
1559
+ stroke-linecap="round" stroke-linejoin="round">
1560
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
1561
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
1562
+ </svg> COPY`;
1563
+
1564
  const formData = new FormData();
1565
  formData.append('image', currentFile);
1566
  formData.append('category', categorySelect.value);
 
1602
  }
1603
 
1604
  dotOut.classList.add('active');
1605
+
1606
+ // ── Attempt grounding overlay for Point / Detect ──
1607
  const cat = categorySelect.value;
1608
  if ((cat === 'Point' || cat === 'Detect') && fullText.trim()) {
1609
+ const parsed = extractGroundingJSON(fullText);
1610
+ if (parsed) {
1611
+ dotGnd.classList.add('active');
1612
+ drawGrounding(URL.createObjectURL(currentFile), fullText);
1613
+ }
1614
  }
1615
 
1616
  } catch (err) {