Files changed (1) hide show
  1. index.html +343 -590
index.html CHANGED
@@ -9,7 +9,7 @@
9
  *{box-sizing:border-box;margin:0;padding:0}
10
  :root{
11
  --bg:#0a0a0c;--panel:#131316;--panel2:#0e0e11;--canvas:#08080a;
12
- --border:#2a2a23;--border2:#3f3f2f;
13
  --text:#e4e4e7;--muted:#a1a1aa;--dim:#6b6b75;--faint:#4a4a52;
14
  --accent:#FFA500;--accent2:#FFB733;--accent3:#FFCC66;
15
  --menubar-h:48px;--statusbar-h:30px;--rail-w:52px;--inspector-w:360px;--filmstrip-h:132px;
@@ -115,6 +115,14 @@ body{
115
  font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--faint);
116
  overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex:1;
117
  }
 
 
 
 
 
 
 
 
118
 
119
  .canvas-stage{
120
  flex:1;position:relative;overflow:hidden;min-height:0;
@@ -129,6 +137,7 @@ body{
129
  border-radius:4px;image-rendering:auto;
130
  animation:canvasIn .3s ease;
131
  }
 
132
  .canvas-stage img.modern-out-img{cursor:crosshair}
133
  @keyframes canvasIn{from{opacity:0;transform:scale(.985)}to{opacity:1;transform:scale(1)}}
134
  .canvas-placeholder{
@@ -143,6 +152,13 @@ body{
143
  border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted);
144
  }
145
 
 
 
 
 
 
 
 
146
  /* Loader */
147
  .modern-loader{
148
  display:none;position:absolute;inset:0;background:rgba(8,8,10,.9);
@@ -259,29 +275,6 @@ body{
259
  }
260
  @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
261
 
262
- .suggestions-wrap{display:flex;flex-wrap:wrap;gap:5px}
263
- .suggestion-chip{
264
- display:inline-flex;align-items:center;padding:4px 11px;
265
- background:rgba(255,165,0,.07);border:1px solid rgba(255,165,0,.18);border-radius:14px;
266
- color:var(--accent3);font-size:11px;font-weight:500;font-family:'Inter',sans-serif;
267
- cursor:pointer;transition:all .15s;white-space:nowrap;
268
- }
269
- .suggestion-chip:hover{background:rgba(255,165,0,.15);border-color:rgba(255,165,0,.35);color:var(--accent2)}
270
- .suggestion-chip.active{background:rgba(255,165,0,.25);border-color:var(--accent);color:#fff}
271
-
272
- .lora-native-select{
273
- width:100%;background:var(--panel2);
274
- border:1px solid var(--border2);border-radius:8px;
275
- padding:9px 34px 9px 13px;
276
- font-family:'Inter',sans-serif;font-size:12px;font-weight:600;color:var(--text);
277
- outline:none;appearance:none;-webkit-appearance:none;
278
- background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23FFA500' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
279
- background-repeat:no-repeat;background-position:right 11px center;
280
- cursor:pointer;transition:border-color .2s;color-scheme:dark;
281
- }
282
- .lora-native-select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,165,0,.15)}
283
- .lora-native-select option{background:var(--panel);color:var(--text)}
284
-
285
  .btn-run{
286
  display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
287
  background:linear-gradient(135deg,#FFA500,#CC8400);border:none;border-radius:9px;
@@ -295,13 +288,13 @@ body{
295
  }
296
  .btn-run:active:not(:disabled){transform:translateY(0)}
297
  .btn-run:disabled{opacity:.55;cursor:not-allowed}
298
- .btn-cancel{
299
  display:flex;align-items:center;justify-content:center;gap:6px;width:100%;
300
  margin-top:8px;background:transparent;border:1px solid var(--border2);border-radius:9px;
301
  padding:8px 24px;cursor:pointer;font-size:12px;font-weight:600;font-family:'Inter',sans-serif;
302
  color:var(--muted);transition:all .15s;
303
  }
304
- .btn-cancel:hover{border-color:#ef4444;color:#ef4444;background:rgba(239,68,68,.08)}
305
  .run-hint{
306
  text-align:center;font-size:10px;color:var(--faint);margin-top:8px;
307
  font-family:'JetBrains Mono',monospace;
@@ -389,16 +382,6 @@ body{
389
  .toast-notification.info{background:linear-gradient(135deg,#FFA500,#CC8400);color:#fff;border:1px solid rgba(255,255,255,.15)}
390
  .toast-notification .toast-icon{font-size:15px;line-height:1}
391
 
392
- /* ══ Lightbox ══ */
393
- .lightbox{
394
- position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:10000;
395
- display:none;align-items:center;justify-content:center;cursor:zoom-out;
396
- backdrop-filter:blur(6px);animation:fadeIn .2s ease;
397
- }
398
- .lightbox.visible{display:flex}
399
- .lightbox img{max-width:94vw;max-height:94vh;border-radius:6px;box-shadow:0 20px 60px rgba(0,0,0,.8)}
400
- @keyframes fadeIn{from{opacity:0}to{opacity:1}}
401
-
402
  /* ══ Drop overlay ══ */
403
  .drop-overlay{
404
  position:fixed;inset:0;z-index:9000;background:rgba(8,8,10,.85);
@@ -417,13 +400,6 @@ body{
417
  ::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
418
  ::-webkit-scrollbar-thumb:hover{background:var(--border2)}
419
 
420
- /* ══ Touch devices ══ */
421
- @media (pointer:coarse){
422
- .fs-remove{display:flex}
423
- .rail-btn{width:44px;height:44px}
424
- .rail-btn svg{width:19px;height:19px}
425
- }
426
-
427
  /* ══ Responsive ══ */
428
  @media(max-width:960px){
429
  html,body{height:auto}
@@ -431,10 +407,7 @@ body{
431
  .studio{
432
  display:flex;flex-direction:column;height:auto;min-height:100vh;
433
  }
434
- .menubar{
435
- flex-wrap:wrap;gap:8px;padding:10px 12px;min-height:var(--menubar-h);
436
- position:sticky;top:0;
437
- }
438
  .menubar-title{font-size:13px}
439
  .menubar-status{display:none}
440
  .menubar-spacer{flex:1}
@@ -467,7 +440,6 @@ body{
467
  .insp-section{padding:14px 16px}
468
  .modern-textarea{font-size:16px}
469
  .btn-run{padding:14px 24px;font-size:15px}
470
- .suggestion-chip{padding:6px 13px;font-size:12px}
471
  .statusbar{flex-wrap:wrap;height:auto;min-height:var(--statusbar-h);padding:4px 10px;gap:2px}
472
  .sb-link{display:none}
473
  .toast-notification{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px}
@@ -475,12 +447,6 @@ body{
475
  .example-card{flex-direction:column}
476
  .example-thumb-wrap{width:100%;height:96px}
477
  }
478
-
479
- @media(max-width:400px){
480
- .menubar-badge.fast{display:none}
481
- .seg-btn{padding:4px 12px}
482
- .canvas-stage{min-height:36vh}
483
- }
484
  </style>
485
  </head>
486
  <body>
@@ -489,11 +455,10 @@ body{
489
  <!-- ══ Menu bar ══ -->
490
  <div class="menubar">
491
  <div class="menubar-logo">
492
- <svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.236L19.764 8 12 11.764 4.236 8 12 4.236zM4 9.618l7 3.5v7.764l-7-3.5V9.618zm9 11.264v-7.764l7-3.5v7.764l-7 3.5z"/></svg>
493
  </div>
494
  <div class="menubar-title">SAM3 Studio <span>/ Segment Anything</span></div>
495
- <span class="menubar-badge">MODEL 3</span>
496
- <span class="menubar-badge fast">ZERO GPU</span>
497
  <div class="menubar-spacer"></div>
498
  <div class="menubar-status"><span class="dot" id="conn-dot"></span><span id="conn-text">connecting…</span></div>
499
  <a href="https://github.com/PRITHIVSAKTHIUR/SAM3-Demo"
@@ -506,51 +471,50 @@ body{
506
  <!-- ══ Left icon rail ══ -->
507
  <div class="rail">
508
  <div class="rail-label">Input</div>
509
- <button class="rail-btn" id="tb-upload" title="Upload Image/Video">
510
  <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
511
  </button>
512
- <button class="rail-btn" id="tb-remove" title="Remove Media" disabled>
513
  <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
514
  </button>
515
- <button class="rail-btn" id="tb-clear" title="Clear Media" disabled>
516
  <svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
517
  </button>
518
  <div class="rail-sep"></div>
519
  <div class="rail-label">Result</div>
520
- <button class="rail-btn" id="dl-btn-output" title="Download Result" disabled>
521
  <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
522
  </button>
523
- <button class="rail-btn" id="zoom-btn" title="Zoom Result" disabled>
524
- <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
525
- </button>
526
- <button class="rail-btn" id="reset-clicks-btn" title="Reset Clicks" disabled>
527
- <svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9"/><polyline points="3 4 3 10 9 10"/></svg>
528
- </button>
529
  </div>
530
 
531
  <!-- ══ Workspace ══ -->
532
  <div class="workspace">
533
  <div class="canvas-head">
 
 
 
 
534
  <div class="canvas-meta" id="canvas-meta">No media loaded</div>
 
535
  </div>
536
 
537
  <div class="canvas-stage" id="output-image-container">
538
  <div class="modern-loader" id="output-loader">
539
  <div class="loader-spinner"></div>
540
- <div class="loader-text" id="loader-text">Processing&hellip;</div>
541
  <div class="loader-bar-track"><div class="loader-bar-fill" id="loader-bar-fill"></div></div>
542
  </div>
543
 
544
  <div class="canvas-placeholder" id="output-placeholder">
545
  <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
546
- <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#3f3f2f" stroke-width="2" stroke-dasharray="4 3"/>
547
- <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(255,165,0,0.1)" stroke="#3f3f2f" stroke-width="1.5"/>
548
- <circle cx="28" cy="30" r="6" fill="rgba(255,165,0,0.12)" stroke="#3f3f2f" stroke-width="1.5"/>
549
  </svg>
550
  <div class="cp-title">Canvas is empty</div>
551
  <div class="cp-sub">
552
  Drop an image or video, paste with <kbd>⌘/Ctrl+V</kbd>, or use the rail to upload.
553
- Then configure settings and press <kbd>⌘/Ctrl</kbd>+<kbd>↵</kbd>.
554
  </div>
555
  </div>
556
  </div>
@@ -571,56 +535,73 @@ body{
571
  <!-- ══ Inspector ══ -->
572
  <div class="inspector">
573
  <div class="inspector-tabs">
574
- <button class="insp-tab active" id="tab-edit">Inference</button>
 
 
575
  <button class="insp-tab" id="tab-examples">Examples</button>
576
  </div>
577
 
578
- <!-- Edit page -->
579
- <div class="insp-page active" id="page-edit">
580
  <div class="insp-section">
581
- <div class="insp-section-title"><span>Task Mode</span></div>
582
- <select id="custom-task-select" class="lora-native-select">
583
- <option value="Image Text">Image Segmentation (Text Prompt)</option>
584
- <option value="Image Click">Image Segmentation (Interactive Clicks)</option>
585
- <option value="Video Text">Video Segmentation (Text Prompt)</option>
586
- </select>
587
  </div>
588
-
589
  <div class="insp-section">
590
- <div class="insp-section-title"><span>Instruction</span><span class="char-count" id="char-count">0</span></div>
591
- <textarea id="custom-prompt-input" class="modern-textarea" rows="3"
592
- placeholder="e.g., player in white, cat, red car..."></textarea>
 
 
 
593
  </div>
594
-
595
  <div class="insp-section">
596
- <div class="insp-section-title"><span>Quick Prompts</span></div>
597
- <div class="suggestions-wrap" id="suggestions-wrap"></div>
598
  </div>
 
599
 
 
 
600
  <div class="insp-section">
601
- <button id="custom-run-btn" class="btn-run">
602
- <span id="run-btn-label">Run Inference</span>
603
- </button>
604
- <button id="custom-cancel-btn" class="btn-cancel" style="display:none;">Cancel</button>
605
- <div class="run-hint">⌘/Ctrl + Enter</div>
606
  </div>
607
-
608
  <div class="insp-section">
609
- <div class="insp-section-title"><span>Advanced</span></div>
610
  <div class="slider-row">
611
- <label>Conf Thresh</label>
612
- <input type="range" id="custom-conf" min="0.0" max="1.0" step="0.05" value="0.45">
613
- <span class="slider-val" id="custom-conf-val">0.45</span>
614
  </div>
615
  <div class="slider-row">
616
- <label>Vid Frames</label>
617
- <input type="range" id="custom-frames" min="10" max="500" step="10" value="60">
618
- <span class="slider-val" id="custom-frames-val">60</span>
619
  </div>
620
  </div>
 
 
 
 
621
  </div>
622
 
623
- <!-- Examples page -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  <div class="insp-page" id="page-examples">
625
  <div class="examples-list" id="examples-scroll"></div>
626
  </div>
@@ -628,7 +609,7 @@ body{
628
 
629
  <!-- ══ Status bar ══ -->
630
  <div class="statusbar">
631
- <div class="sb-section" id="sb-image-count">No input</div>
632
  <div class="sb-right">
633
  <a class="sb-link" href="https://huggingface.co/facebook/sam3" target="_blank" rel="noopener">facebook/sam3</a>
634
  <span class="sb-pill" id="sb-status">Ready</span>
@@ -637,71 +618,63 @@ body{
637
 
638
  </div>
639
 
640
- <input id="custom-file-input" type="file" accept="image/*,video/mp4" style="display:none;" />
641
-
642
- <!-- Lightbox -->
643
- <div class="lightbox" id="lightbox"><img id="lightbox-img" alt="zoom"></div>
644
-
645
- <!-- Global drop overlay -->
646
  <div class="drop-overlay" id="drop-overlay"><div class="drop-overlay-inner">Drop media to upload</div></div>
647
 
648
  <script type="module">
649
  import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
650
 
651
- /* ── DOM refs ── */
652
  const galleryGrid = document.getElementById('image-gallery-grid');
653
  const fileInput = document.getElementById('custom-file-input');
654
  const btnUpload = document.getElementById('tb-upload');
655
  const btnRemove = document.getElementById('tb-remove');
656
  const btnClear = document.getElementById('tb-clear');
657
- const btnResetClicks = document.getElementById('reset-clicks-btn');
658
- const promptInput = document.getElementById('custom-prompt-input');
659
- const charCount = document.getElementById('char-count');
660
- const taskSelect = document.getElementById('custom-task-select');
661
- const runBtn = document.getElementById('custom-run-btn');
662
- const runBtnLabel = document.getElementById('run-btn-label');
663
- const cancelBtn = document.getElementById('custom-cancel-btn');
664
- const imgCountTb = document.getElementById('tb-image-count');
665
- const imgCountSb = document.getElementById('sb-image-count');
666
  const sbStatus = document.getElementById('sb-status');
667
  const loader = document.getElementById('output-loader');
668
  const loaderText = document.getElementById('loader-text');
669
  const loaderFill = document.getElementById('loader-bar-fill');
670
- const outBody = document.getElementById('output-image-container');
671
- const outPh = document.getElementById('output-placeholder');
672
- const dlBtn = document.getElementById('dl-btn-output');
673
- const zoomBtn = document.getElementById('zoom-btn');
674
  const historyStrip = document.getElementById('history-strip');
675
  const historyCount = document.getElementById('history-count');
676
- const confSlider = document.getElementById('custom-conf');
677
- const confVal = document.getElementById('custom-conf-val');
678
- const framesSlider = document.getElementById('custom-frames');
679
- const framesVal = document.getElementById('custom-frames-val');
680
- const lightbox = document.getElementById('lightbox');
681
- const lightboxImg = document.getElementById('lightbox-img');
682
  const dropOverlay = document.getElementById('drop-overlay');
683
  const canvasMeta = document.getElementById('canvas-meta');
 
 
684
  const connDot = document.getElementById('conn-dot');
685
  const connText = document.getElementById('conn-text');
686
 
687
- /* ── State ── */
688
- let currentMedia = null; // {b64, name, type: 'image'|'video'}
689
- let toastTimer = null;
690
- let running = false;
691
- let currentJob = null;
692
- let currentResult = null; // {b64, type: 'image'|'video'}
 
 
 
 
 
 
 
 
 
 
693
  let history = [];
694
  let historyIdx = -1;
 
 
 
 
695
  let clickPoints = [];
696
 
697
- const SUGGESTIONS = [
698
- ['Person', 'segment the person'],
699
- ['Car', 'segment the red car'],
700
- ['Animal', 'segment the cat'],
701
- ['Background', 'segment the background'],
702
  ];
703
 
704
- /* ── Toast ── */
705
  function showToast(message, type) {
706
  let toast = document.getElementById('app-toast');
707
  if (!toast) {
@@ -716,16 +689,8 @@ function showToast(message, type) {
716
  toast.className = 'toast-notification ' + (type || 'error');
717
  icon.textContent = type === 'warning' ? '⚠' : type === 'info' ? 'ℹ' : '✗';
718
  text.textContent = message;
719
- if (toastTimer) clearTimeout(toastTimer);
720
- void toast.offsetWidth;
721
  toast.classList.add('visible');
722
- toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
723
- }
724
-
725
- function flashPromptError() {
726
- promptInput.classList.add('error-flash');
727
- promptInput.focus();
728
- setTimeout(() => promptInput.classList.remove('error-flash'), 800);
729
  }
730
 
731
  function setStatus(text, cls) {
@@ -733,44 +698,55 @@ function setStatus(text, cls) {
733
  sbStatus.className = 'sb-pill' + (cls ? ' ' + cls : '');
734
  }
735
 
736
- /* ── Canvas Rendering ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  function renderCanvas() {
738
- outBody.querySelectorAll('video.modern-out-vid, img.modern-out-img').forEach(e => e.remove());
 
 
 
739
 
740
- const showResult = currentResult;
741
- const showInput = currentMedia;
 
742
 
743
  if (showResult || showInput) {
744
  outPh.style.display = 'none';
 
745
 
746
- const media = showResult || showInput;
747
-
748
- if (media.type === 'video') {
749
- const vid = document.createElement('video');
750
- vid.src = media.b64;
751
- vid.className = 'modern-out-vid';
752
- vid.controls = true;
753
- vid.autoplay = true;
754
- vid.loop = true;
755
- vid.muted = true;
756
- outBody.appendChild(vid);
757
- canvasMeta.textContent = (showResult ? 'result · ' : 'input · ') + (media.name || 'video');
758
  } else {
759
- const img = document.createElement('img');
760
- img.src = media.b64;
761
- img.className = 'modern-out-img';
762
- img.addEventListener('click', (e) => {
763
- if (taskSelect.value === 'Image Click' && !showResult) {
764
- handleImageClick(e, img);
765
- } else {
766
- lightboxImg.src = img.src;
767
- lightbox.classList.add('visible');
768
- }
769
- });
770
- outBody.appendChild(img);
771
- canvasMeta.textContent = (showResult ? 'result · ' : 'input · ') + (media.name || 'image');
772
  }
 
773
  } else {
 
774
  outPh.style.display = '';
775
  canvasMeta.textContent = 'No media loaded';
776
  }
@@ -778,38 +754,27 @@ function renderCanvas() {
778
  }
779
 
780
  function updateRailState() {
781
- const hasResult = !!currentResult;
782
  const hasMedia = !!currentMedia;
783
- dlBtn.disabled = !hasResult;
784
- zoomBtn.disabled = !hasResult && !hasMedia;
785
  btnRemove.disabled = !hasMedia;
786
- btnClear.disabled = !hasMedia;
787
- btnResetClicks.disabled = clickPoints.length === 0;
788
- }
789
-
790
- /* ── Inputs filmstrip ── */
791
- function updateCounts() {
792
- const n = currentMedia ? 1 : 0;
793
- imgCountTb.textContent = n;
794
- imgCountSb.textContent = n > 0 ? '1 input · ' + history.length + ' result' + (history.length === 1 ? '' : 's') : 'No input';
795
  }
796
 
797
- function setMedia(b64, name, type) {
798
- currentMedia = { b64, name: name || 'media', type };
799
- currentResult = null;
800
- clickPoints = [];
801
- renderFilmstrip();
802
- updateCounts();
803
  renderCanvas();
804
  }
 
 
805
 
806
- function removeMedia() {
807
- currentMedia = null;
808
  currentResult = null;
809
- clickPoints = [];
810
  renderFilmstrip();
811
- updateCounts();
812
- renderCanvas();
813
  }
814
 
815
  function clearAll() {
@@ -817,7 +782,6 @@ function clearAll() {
817
  currentResult = null;
818
  clickPoints = [];
819
  renderFilmstrip();
820
- updateCounts();
821
  renderCanvas();
822
  }
823
 
@@ -831,237 +795,128 @@ function renderFilmstrip() {
831
  } else {
832
  const t = document.createElement('div');
833
  t.className = 'fs-thumb selected';
834
- // Use first frame if video? For simplicity, just show a placeholder for video in filmstrip
835
- if (currentMedia.type === 'image') {
836
- t.innerHTML = '<img src="' + currentMedia.b64 + '" alt="">' +
837
- '<span class="fs-badge">' + (currentResult ? 'input' : 'input') + '</span>' +
838
- '<button class="fs-remove">✕</button>';
839
- } else {
840
- t.innerHTML = '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#000;color:#fff;font-size:10px;">VIDEO</div>' +
841
- '<span class="fs-badge">input</span>' +
842
- '<button class="fs-remove">✕</button>';
843
- }
844
- t.addEventListener('click', (e) => {
845
- if (e.target.closest('.fs-remove')) return;
846
- currentResult = null;
847
- renderCanvas();
848
- });
849
- t.querySelector('.fs-remove').addEventListener('click', (e) => {
850
- e.stopPropagation(); removeMedia();
851
- });
852
  galleryGrid.appendChild(t);
853
  }
854
  const add = document.createElement('div');
855
  add.className = 'fs-add';
856
- add.innerHTML = '<span class="add-icon">+</span><span class="add-text">' + (currentMedia ? 'Replace' : 'Add') + '</span>';
857
  add.addEventListener('click', () => fileInput.click());
858
  galleryGrid.appendChild(add);
859
- updateRailState();
860
  }
861
 
862
- function processFiles(files) {
863
- const file = files && files[0];
864
- if (!file) return 0;
865
-
866
- const isVideo = file.type.startsWith('video/');
867
- const isImage = file.type.startsWith('image/');
868
- if (!isVideo && !isImage) return 0;
869
-
870
  const reader = new FileReader();
 
871
  reader.onload = (e) => {
872
- const type = isVideo ? 'video' : 'image';
873
- setMedia(e.target.result, file.name, type);
874
  if (type === 'image') {
875
- taskSelect.value = 'Image Text';
 
 
 
 
 
876
  } else {
877
- taskSelect.value = 'Video Text';
878
  }
879
- showToast(type.charAt(0).toUpperCase() + type.slice(1) + ' loaded', 'info');
880
  };
881
  reader.readAsDataURL(file);
882
- return 1;
883
  }
884
 
885
- fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; });
886
  btnUpload.addEventListener('click', () => fileInput.click());
887
- btnRemove.addEventListener('click', removeMedia);
888
- btnClear.addEventListener('click', clearAll);
889
- btnResetClicks.addEventListener('click', () => {
890
- clickPoints = [];
891
- if (currentMedia && currentMedia.type === 'image') {
892
- currentResult = null;
893
- renderCanvas();
894
- showToast('Clicks reset', 'info');
895
- }
896
- updateRailState();
897
- });
898
 
899
- /* ── Drop anywhere ── */
900
  let dragDepth = 0;
901
  window.addEventListener('dragenter', (e) => {
902
- if (!e.dataTransfer || !Array.from(e.dataTransfer.types).includes('Files')) return;
903
- e.preventDefault();
904
- dragDepth++;
905
- dropOverlay.classList.add('visible');
906
  });
907
  window.addEventListener('dragleave', (e) => {
908
- if (!e.dataTransfer || !Array.from(e.dataTransfer.types).includes('Files')) return;
909
- dragDepth = Math.max(0, dragDepth - 1);
910
  if (dragDepth === 0) dropOverlay.classList.remove('visible');
911
  });
912
  window.addEventListener('dragover', (e) => { e.preventDefault(); });
913
  window.addEventListener('drop', (e) => {
914
- e.preventDefault();
915
- dragDepth = 0;
916
- dropOverlay.classList.remove('visible');
917
  if (e.dataTransfer.files.length) {
918
- const n = processFiles(e.dataTransfer.files);
919
  }
920
  });
921
 
922
- /* ── Paste ── */
923
- document.addEventListener('paste', (e) => {
924
- if (e.target === promptInput) return;
925
- const items = e.clipboardData && e.clipboardData.items;
926
- if (!items) return;
927
- for (const item of items) {
928
- if (item.kind === 'file') {
929
- const f = item.getAsFile();
930
- if (f) {
931
- e.preventDefault();
932
- processFiles([f]);
933
- return;
934
- }
 
 
 
 
 
935
  }
936
- }
937
- });
938
-
939
- /* ── Inspector tabs ── */
940
- const tabEdit = document.getElementById('tab-edit');
941
- const tabExamples = document.getElementById('tab-examples');
942
- const pageEdit = document.getElementById('page-edit');
943
- const pageExamples = document.getElementById('page-examples');
944
- tabEdit.addEventListener('click', () => {
945
- tabEdit.classList.add('active'); tabExamples.classList.remove('active');
946
- pageEdit.classList.add('active'); pageExamples.classList.remove('active');
947
- });
948
- tabExamples.addEventListener('click', () => {
949
- tabExamples.classList.add('active'); tabEdit.classList.remove('active');
950
- pageExamples.classList.add('active'); pageEdit.classList.remove('active');
951
- });
952
-
953
- /* ── Suggestion chips ── */
954
- const suggWrap = document.getElementById('suggestions-wrap');
955
- SUGGESTIONS.forEach(([label, prompt]) => {
956
- const chip = document.createElement('button');
957
- chip.className = 'suggestion-chip';
958
- chip.textContent = label;
959
- chip.addEventListener('click', () => {
960
- promptInput.value = prompt;
961
- updateCharCount();
962
- suggWrap.querySelectorAll('.suggestion-chip').forEach(c => c.classList.remove('active'));
963
- chip.classList.add('active');
964
- promptInput.focus();
965
  });
966
- suggWrap.appendChild(chip);
967
  });
968
 
969
- function updateCharCount() { charCount.textContent = promptInput.value.length; }
970
- promptInput.addEventListener('input', () => {
971
- updateCharCount();
972
- suggWrap.querySelectorAll('.suggestion-chip').forEach(c => c.classList.remove('active'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
973
  });
974
- updateCharCount();
975
-
976
- /* ── Sliders ── */
977
- function bindSlider(id, suffix='') {
978
- const slider = document.getElementById(id);
979
- const valSpan = document.getElementById(id + '-val');
980
- slider.addEventListener('input', () => { valSpan.textContent = slider.value + suffix; });
981
- }
982
- bindSlider('custom-conf');
983
- bindSlider('custom-frames');
984
-
985
- /* ── Loader ── */
986
- function showLoader(text) {
987
- loaderText.textContent = text || 'Processing…';
988
- loaderFill.classList.remove('determinate');
989
- loaderFill.style.width = '100%';
990
- loader.classList.add('active');
991
- setStatus('Processing…');
992
- }
993
- function setLoaderProgress(text, frac) {
994
- loaderText.textContent = text;
995
- if (frac != null && frac >= 0 && frac <= 1) {
996
- loaderFill.classList.add('determinate');
997
- loaderFill.style.width = Math.max(4, Math.round(frac * 100)) + '%';
998
- }
999
- }
1000
- function hideLoader(statusText, cls) {
1001
- loader.classList.remove('active');
1002
- setStatus(statusText || 'Done', cls || 'done');
1003
- }
1004
 
1005
- /* ── Result / history ── */
1006
- function renderHistory() {
1007
- historyStrip.innerHTML = '';
1008
- historyCount.textContent = history.length;
1009
- if (history.length === 0) {
1010
- const empty = document.createElement('div');
1011
- empty.className = 'fs-empty';
1012
- empty.textContent = 'No results yet';
1013
- historyStrip.appendChild(empty);
1014
- return;
1015
- }
1016
- history.forEach((entry, i) => {
1017
- const t = document.createElement('div');
1018
- t.className = 'fs-thumb' + (i === historyIdx ? ' selected' : '');
1019
- if (entry.type === 'video') {
1020
- t.innerHTML = '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#000;color:#fff;font-size:10px;">VID</div>';
1021
- } else {
1022
- const im = document.createElement('img');
1023
- im.src = entry.b64;
1024
- t.appendChild(im);
1025
- }
1026
- t.addEventListener('click', () => {
1027
- historyIdx = i;
1028
- currentResult = history[i];
1029
- renderHistory();
1030
- renderCanvas();
1031
- });
1032
- historyStrip.appendChild(t);
1033
- });
1034
- }
1035
-
1036
- function pushHistory(entry) {
1037
- history.push(entry);
1038
- historyIdx = history.length - 1;
1039
- renderHistory();
1040
- updateCounts();
1041
- }
1042
-
1043
- /* ── Lightbox ── */
1044
- lightbox.addEventListener('click', () => lightbox.classList.remove('visible'));
1045
- document.addEventListener('keydown', (e) => { if (e.key === 'Escape') lightbox.classList.remove('visible'); });
1046
- zoomBtn.addEventListener('click', () => {
1047
- const media = currentResult || currentMedia;
1048
- if (media && media.type === 'image') {
1049
- lightboxImg.src = media.b64;
1050
- lightbox.classList.add('visible');
1051
  }
1052
  });
1053
 
1054
- /* ── Download ── */
 
 
 
 
 
1055
  dlBtn.addEventListener('click', () => {
1056
- if (currentResult && currentResult.b64) {
1057
- const a = document.createElement('a');
1058
- a.href = currentResult.b64;
1059
- a.download = 'sam3_result.' + (currentResult.type === 'video' ? 'mp4' : 'png');
1060
- document.body.appendChild(a); a.click(); document.body.removeChild(a);
1061
- }
1062
  });
1063
 
1064
- /* ── Backend connection ── */
1065
  let client = null;
1066
  try {
1067
  client = await Client.connect(window.location.origin);
@@ -1069,248 +924,146 @@ try {
1069
  connDot.classList.remove('off');
1070
  connText.textContent = 'connected';
1071
  } catch (e) {
1072
- console.error('Failed to connect to Gradio backend:', e);
1073
  setStatus('Offline', 'error');
1074
  connDot.classList.add('off');
1075
  connText.textContent = 'offline';
1076
- showToast('Could not connect to the backend API', 'error');
1077
- }
1078
-
1079
- /* ── Config: examples ── */
1080
- function renderConfig(cfg) {
1081
- const list = document.getElementById('examples-scroll');
1082
- list.innerHTML = '';
1083
- cfg.examples.forEach(ex => {
1084
- const card = document.createElement('div');
1085
- card.className = 'example-card';
1086
- const thumbHtml = ex.thumb
1087
- ? '<img src="' + ex.thumb + '" alt="">'
1088
- : '<div class="example-thumb-placeholder">Preview</div>';
1089
- const promptShort = ex.prompt.length > 90 ? ex.prompt.slice(0, 90) + '…' : ex.prompt;
1090
- card.innerHTML =
1091
- '<div class="example-thumb-wrap">' + thumbHtml + '</div>'
1092
- + '<div class="example-body">'
1093
- + '<div class="example-meta"><span class="example-lora-badge">' + ex.task + '</span></div>'
1094
- + '<div class="example-prompt-text"></div>'
1095
- + '</div>';
1096
- card.querySelector('.example-prompt-text').textContent = promptShort;
1097
- card.addEventListener('click', () => loadExample(card, ex.idx));
1098
- list.appendChild(card);
1099
- });
1100
- }
1101
-
1102
- async function loadExample(card, idx) {
1103
- if (!client) { showToast('Backend not connected', 'error'); return; }
1104
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1105
- card.classList.add('loading');
1106
- showToast('Loading example…', 'info');
1107
- try {
1108
- const result = await client.predict('/load_example', { idx: idx });
1109
- const data = result.data[0];
1110
- if (data && data.status === 'ok' && data.image) {
1111
- if (data.prompt) { promptInput.value = data.prompt; updateCharCount(); }
1112
- if (data.task) {
1113
- taskSelect.value = data.task;
1114
- }
1115
- setMedia(data.image, data.name || 'example.jpg', 'image');
1116
- tabEdit.click();
1117
- showToast('Example loaded', 'info');
1118
- } else {
1119
- showToast('Could not load example image', 'error');
1120
- }
1121
- } catch (e) {
1122
- console.error('Example load error:', e);
1123
- showToast('Could not load example image', 'error');
1124
- } finally {
1125
- card.classList.remove('loading');
1126
- }
1127
- }
1128
-
1129
- try {
1130
- const cfg = await fetch('api/config').then(r => r.json());
1131
- renderConfig(cfg);
1132
- } catch (e) {
1133
- console.error('Failed to load config:', e);
1134
- }
1135
-
1136
- /* ── Run ── */
1137
- function validateBeforeRun() {
1138
- const promptVal = promptInput.value.trim();
1139
- const taskVal = taskSelect.value;
1140
- const hasMedia = !!currentMedia;
1141
-
1142
- if (!hasMedia) { showToast('Please upload media', 'error'); return false; }
1143
-
1144
- if (taskVal !== 'Image Click' && !promptVal) {
1145
- showToast('Please enter a prompt', 'warning'); flashPromptError(); return false;
1146
- }
1147
-
1148
- if (taskVal === 'Image Click' && currentMedia.type !== 'image') {
1149
- showToast('Click segmentation requires an image', 'error'); return false;
1150
- }
1151
-
1152
- if (taskVal === 'Video Text' && currentMedia.type !== 'video') {
1153
- showToast('Video segmentation requires a video', 'error'); return false;
1154
- }
1155
-
1156
- return true;
1157
  }
1158
 
1159
- function setRunningUI(on) {
1160
- running = on;
1161
- runBtn.disabled = on;
1162
- runBtnLabel.textContent = on ? 'Running…' : 'Run Inference';
1163
- cancelBtn.style.display = on ? 'flex' : 'none';
1164
- }
1165
-
1166
- async function runEdit() {
1167
  if (running) return;
1168
- if (!client) { showToast('Backend not connected', 'error'); return; }
1169
- if (!validateBeforeRun()) return;
1170
-
1171
  setRunningUI(true);
1172
  showLoader('Submitting to queue…');
1173
 
1174
- const taskVal = taskSelect.value;
1175
- const usedPrompt = promptInput.value;
1176
-
1177
- let apiEndpoint = '/segment_image_text';
1178
- let payload = {};
1179
-
1180
- if (taskVal === 'Image Text') {
1181
- apiEndpoint = '/segment_image_text';
1182
- payload = {
1183
- image_b64: currentMedia.b64,
1184
- prompt: usedPrompt,
1185
- conf_thresh: parseFloat(confSlider.value)
1186
- };
1187
- } else if (taskVal === 'Video Text') {
1188
- apiEndpoint = '/segment_video';
1189
- payload = {
1190
- video_b64: currentMedia.b64,
1191
- prompt: usedPrompt,
1192
- frame_limit: parseInt(framesSlider.value),
1193
- time_limit: 120
1194
- };
1195
- } else if (taskVal === 'Image Click') {
1196
- // This is handled automatically by clicks, but we can allow a manual run to refresh
1197
- apiEndpoint = '/segment_image_click';
1198
- payload = {
1199
- image_b64: currentMedia.b64,
1200
- points_json: JSON.stringify(clickPoints),
1201
- labels_json: JSON.stringify(clickPoints.map(() => 1))
1202
- };
1203
- }
1204
-
1205
  try {
1206
- currentJob = client.submit(apiEndpoint, payload);
1207
  for await (const msg of currentJob) {
1208
  if (msg.type === 'status') {
1209
  if (msg.status === 'pending') {
1210
- const pos = (msg.position != null && msg.position >= 0)
1211
- ? 'In queue — position ' + (msg.position + 1) + (msg.queue_size ? ' of ' + msg.queue_size : '')
1212
- : 'Waiting in queue…';
1213
- setLoaderProgress(pos, null);
1214
  setStatus('Queued');
1215
  } else if (msg.status === 'generating') {
1216
  setStatus('Processing…');
1217
- setLoaderProgress('Generating…', null);
1218
  } else if (msg.status === 'error') {
1219
- throw new Error(msg.message || 'Generation failed');
1220
  }
1221
  } else if (msg.type === 'data') {
1222
- const out = msg.data && msg.data[0];
1223
- if (out && (out.image || out.video)) {
1224
- const entry = out.video
1225
- ? { b64: out.video, type: 'video' }
1226
- : { b64: out.image, type: 'image' };
1227
- currentResult = entry;
1228
- pushHistory(entry);
1229
  hideLoader('Done', 'done');
1230
- showToast('Inference successful', 'info');
1231
- renderCanvas();
1232
- } else {
 
1233
  hideLoader('Done', 'done');
 
 
1234
  }
1235
  }
1236
  }
1237
  } catch (e) {
1238
- console.error('Edit error:', e);
1239
  hideLoader('Error', 'error');
1240
- const msg = (e && e.message) ? e.message : 'Generation failed';
1241
- showToast(msg.length > 120 ? msg.slice(0, 120) + '…' : msg, 'error');
1242
  } finally {
1243
  setRunningUI(false);
1244
  currentJob = null;
1245
  }
1246
  }
1247
 
1248
- // Handle Image Clicks
1249
- async function handleImageClick(e, imgElement) {
1250
- if (running) return;
1251
- if (!currentMedia || currentMedia.type !== 'image') return;
 
 
 
 
 
1252
 
1253
- const rect = imgElement.getBoundingClientRect();
1254
- const x = Math.round((e.clientX - rect.left) / rect.width * imgElement.naturalWidth);
1255
- const y = Math.round((e.clientY - rect.top) / rect.height * imgElement.naturalHeight);
 
 
 
 
1256
 
1257
- clickPoints.push([x, y]);
1258
- btnResetClicks.disabled = false;
 
 
 
 
 
 
 
 
1259
 
1260
- setRunningUI(true);
1261
- showLoader('Processing click…');
 
 
 
 
 
 
 
 
 
1262
 
1263
- const payload = {
 
 
 
1264
  image_b64: currentMedia.b64,
1265
- points_json: JSON.stringify(clickPoints),
1266
- labels_json: JSON.stringify(clickPoints.map(() => 1))
1267
- };
1268
 
1269
- try {
1270
- const result = await client.predict('/segment_image_click', payload);
1271
- const out = result.data && result.data[0];
1272
- if (out && out.image) {
1273
- const entry = { b64: out.image, type: 'image' };
1274
- currentResult = entry;
1275
- // Replace the last history entry if it was a click, otherwise push
1276
- if (history.length > 0 && history[history.length - 1].type === 'image' && history[history.length - 1].b64.startsWith('data:image/png;base64,')) {
1277
- // Heuristic: just update the current view, don't spam history with every click
1278
- // For simplicity, we won't push every click to history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1279
  }
1280
- hideLoader('Done', 'done');
1281
- renderCanvas();
1282
- }
1283
- } catch (err) {
1284
- console.error('Click error:', err);
1285
- hideLoader('Error', 'error');
1286
- showToast('Click processing failed', 'error');
1287
- } finally {
1288
- setRunningUI(false);
1289
- }
1290
  }
1291
 
1292
- runBtn.addEventListener('click', runEdit);
1293
-
1294
- cancelBtn.addEventListener('click', async () => {
1295
- if (currentJob) {
1296
- try { await currentJob.cancel(); } catch (e) { console.warn('Cancel error:', e); }
1297
- currentJob = null;
1298
- }
1299
- setRunningUI(false);
1300
- hideLoader('Cancelled');
1301
- showToast('Generation cancelled', 'warning');
1302
- });
1303
-
1304
- document.addEventListener('keydown', (e) => {
1305
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
1306
- e.preventDefault();
1307
- runEdit();
1308
- }
1309
- });
1310
 
1311
  renderFilmstrip();
1312
- renderHistory();
1313
- updateCounts();
1314
  renderCanvas();
1315
  </script>
1316
  </body>
 
9
  *{box-sizing:border-box;margin:0;padding:0}
10
  :root{
11
  --bg:#0a0a0c;--panel:#131316;--panel2:#0e0e11;--canvas:#08080a;
12
+ --border:#2a241a;--border2:#3f342a;
13
  --text:#e4e4e7;--muted:#a1a1aa;--dim:#6b6b75;--faint:#4a4a52;
14
  --accent:#FFA500;--accent2:#FFB733;--accent3:#FFCC66;
15
  --menubar-h:48px;--statusbar-h:30px;--rail-w:52px;--inspector-w:360px;--filmstrip-h:132px;
 
115
  font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--faint);
116
  overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex:1;
117
  }
118
+ .canvas-actions{display:flex;align-items:center;gap:6px}
119
+ .out-seed-chip{
120
+ display:none;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:500;
121
+ color:var(--dim);background:var(--panel);border:1px solid var(--border);
122
+ border-radius:6px;padding:4px 9px;cursor:pointer;transition:all .15s;
123
+ }
124
+ .out-seed-chip.visible{display:inline-flex}
125
+ .out-seed-chip:hover{color:var(--accent3);border-color:rgba(255,165,0,.35)}
126
 
127
  .canvas-stage{
128
  flex:1;position:relative;overflow:hidden;min-height:0;
 
137
  border-radius:4px;image-rendering:auto;
138
  animation:canvasIn .3s ease;
139
  }
140
+ .canvas-stage video.modern-out-vid{cursor:pointer}
141
  .canvas-stage img.modern-out-img{cursor:crosshair}
142
  @keyframes canvasIn{from{opacity:0;transform:scale(.985)}to{opacity:1;transform:scale(1)}}
143
  .canvas-placeholder{
 
152
  border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted);
153
  }
154
 
155
+ /* Click Points Overlay */
156
+ .click-point{
157
+ position:absolute;width:16px;height:16px;background:#ff0000;
158
+ border:2px solid #ffffff;border-radius:50%;pointer-events:none;
159
+ transform:translate(-50%, -50%);box-shadow:0 0 4px rgba(0,0,0,0.8);
160
+ }
161
+
162
  /* Loader */
163
  .modern-loader{
164
  display:none;position:absolute;inset:0;background:rgba(8,8,10,.9);
 
275
  }
276
  @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
277
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  .btn-run{
279
  display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
280
  background:linear-gradient(135deg,#FFA500,#CC8400);border:none;border-radius:9px;
 
288
  }
289
  .btn-run:active:not(:disabled){transform:translateY(0)}
290
  .btn-run:disabled{opacity:.55;cursor:not-allowed}
291
+ .btn-secondary{
292
  display:flex;align-items:center;justify-content:center;gap:6px;width:100%;
293
  margin-top:8px;background:transparent;border:1px solid var(--border2);border-radius:9px;
294
  padding:8px 24px;cursor:pointer;font-size:12px;font-weight:600;font-family:'Inter',sans-serif;
295
  color:var(--muted);transition:all .15s;
296
  }
297
+ .btn-secondary:hover{border-color:var(--accent);color:var(--accent2);background:rgba(255,165,0,.08)}
298
  .run-hint{
299
  text-align:center;font-size:10px;color:var(--faint);margin-top:8px;
300
  font-family:'JetBrains Mono',monospace;
 
382
  .toast-notification.info{background:linear-gradient(135deg,#FFA500,#CC8400);color:#fff;border:1px solid rgba(255,255,255,.15)}
383
  .toast-notification .toast-icon{font-size:15px;line-height:1}
384
 
 
 
 
 
 
 
 
 
 
 
385
  /* ══ Drop overlay ══ */
386
  .drop-overlay{
387
  position:fixed;inset:0;z-index:9000;background:rgba(8,8,10,.85);
 
400
  ::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
401
  ::-webkit-scrollbar-thumb:hover{background:var(--border2)}
402
 
 
 
 
 
 
 
 
403
  /* ══ Responsive ══ */
404
  @media(max-width:960px){
405
  html,body{height:auto}
 
407
  .studio{
408
  display:flex;flex-direction:column;height:auto;min-height:100vh;
409
  }
410
+ .menubar{flex-wrap:wrap;gap:8px;padding:10px 12px;min-height:var(--menubar-h);position:sticky;top:0;}
 
 
 
411
  .menubar-title{font-size:13px}
412
  .menubar-status{display:none}
413
  .menubar-spacer{flex:1}
 
440
  .insp-section{padding:14px 16px}
441
  .modern-textarea{font-size:16px}
442
  .btn-run{padding:14px 24px;font-size:15px}
 
443
  .statusbar{flex-wrap:wrap;height:auto;min-height:var(--statusbar-h);padding:4px 10px;gap:2px}
444
  .sb-link{display:none}
445
  .toast-notification{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px}
 
447
  .example-card{flex-direction:column}
448
  .example-thumb-wrap{width:100%;height:96px}
449
  }
 
 
 
 
 
 
450
  </style>
451
  </head>
452
  <body>
 
455
  <!-- ══ Menu bar ══ -->
456
  <div class="menubar">
457
  <div class="menubar-logo">
458
+ <svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.18L19.5 8 12 11.82 4.5 8 12 4.18zM4 9.82l7 3.5v7.36l-7-3.5V9.82zm9 10.86v-7.36l7-3.5v7.36l-7 3.5z"/></svg>
459
  </div>
460
  <div class="menubar-title">SAM3 Studio <span>/ Segment Anything</span></div>
461
+ <span class="menubar-badge">Model 3</span>
 
462
  <div class="menubar-spacer"></div>
463
  <div class="menubar-status"><span class="dot" id="conn-dot"></span><span id="conn-text">connecting…</span></div>
464
  <a href="https://github.com/PRITHIVSAKTHIUR/SAM3-Demo"
 
471
  <!-- ══ Left icon rail ══ -->
472
  <div class="rail">
473
  <div class="rail-label">Input</div>
474
+ <button class="rail-btn" id="tb-upload" title="Upload media">
475
  <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
476
  </button>
477
+ <button class="rail-btn" id="tb-remove" title="Remove media" disabled>
478
  <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
479
  </button>
480
+ <button class="rail-btn" id="tb-clear" title="Clear canvas" disabled>
481
  <svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
482
  </button>
483
  <div class="rail-sep"></div>
484
  <div class="rail-label">Result</div>
485
+ <button class="rail-btn" id="dl-btn-output" title="Download result" disabled>
486
  <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
487
  </button>
 
 
 
 
 
 
488
  </div>
489
 
490
  <!-- ══ Workspace ══ -->
491
  <div class="workspace">
492
  <div class="canvas-head">
493
+ <div class="seg-control">
494
+ <button class="seg-btn active" id="seg-result">Result</button>
495
+ <button class="seg-btn" id="seg-input">Input</button>
496
+ </div>
497
  <div class="canvas-meta" id="canvas-meta">No media loaded</div>
498
+ <div class="canvas-actions"></div>
499
  </div>
500
 
501
  <div class="canvas-stage" id="output-image-container">
502
  <div class="modern-loader" id="output-loader">
503
  <div class="loader-spinner"></div>
504
+ <div class="loader-text" id="loader-text">Processing</div>
505
  <div class="loader-bar-track"><div class="loader-bar-fill" id="loader-bar-fill"></div></div>
506
  </div>
507
 
508
  <div class="canvas-placeholder" id="output-placeholder">
509
  <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
510
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#3f342a" stroke-width="2" stroke-dasharray="4 3"/>
511
+ <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(255,165,0,0.1)" stroke="#3f342a" stroke-width="1.5"/>
512
+ <circle cx="28" cy="30" r="6" fill="rgba(255,165,0,0.12)" stroke="#3f342a" stroke-width="1.5"/>
513
  </svg>
514
  <div class="cp-title">Canvas is empty</div>
515
  <div class="cp-sub">
516
  Drop an image or video, paste with <kbd>⌘/Ctrl+V</kbd>, or use the rail to upload.
517
+ Then write an instruction and press <kbd>⌘/Ctrl</kbd>+<kbd>↵</kbd>.
518
  </div>
519
  </div>
520
  </div>
 
535
  <!-- ══ Inspector ══ -->
536
  <div class="inspector">
537
  <div class="inspector-tabs">
538
+ <button class="insp-tab active" id="tab-image">Image</button>
539
+ <button class="insp-tab" id="tab-video">Video</button>
540
+ <button class="insp-tab" id="tab-click">Click</button>
541
  <button class="insp-tab" id="tab-examples">Examples</button>
542
  </div>
543
 
544
+ <!-- Image Page -->
545
+ <div class="insp-page active" id="page-image">
546
  <div class="insp-section">
547
+ <div class="insp-section-title"><span>Instruction</span><span class="char-count" id="char-count-img">0</span></div>
548
+ <textarea id="prompt-image" class="modern-textarea" rows="3" placeholder="e.g., cat, face, car wheel…"></textarea>
 
 
 
 
549
  </div>
 
550
  <div class="insp-section">
551
+ <div class="insp-section-title"><span>Advanced</span></div>
552
+ <div class="slider-row">
553
+ <label>Confidence</label>
554
+ <input type="range" id="conf-slider" min="0.0" max="1.0" step="0.05" value="0.45">
555
+ <span class="slider-val" id="conf-slider-val">0.45</span>
556
+ </div>
557
  </div>
 
558
  <div class="insp-section">
559
+ <button id="btn-run-image" class="btn-run">Segment Image</button>
560
+ <div class="run-hint">⌘/Ctrl + Enter</div>
561
  </div>
562
+ </div>
563
 
564
+ <!-- Video Page -->
565
+ <div class="insp-page" id="page-video">
566
  <div class="insp-section">
567
+ <div class="insp-section-title"><span>Instruction</span><span class="char-count" id="char-count-vid">0</span></div>
568
+ <textarea id="prompt-video" class="modern-textarea" rows="3" placeholder="e.g., person running, red car…"></textarea>
 
 
 
569
  </div>
 
570
  <div class="insp-section">
571
+ <div class="insp-section-title"><span>Limits</span></div>
572
  <div class="slider-row">
573
+ <label>Max Frames</label>
574
+ <input type="range" id="frames-slider" min="10" max="500" step="10" value="60">
575
+ <span class="slider-val" id="frames-slider-val">60</span>
576
  </div>
577
  <div class="slider-row">
578
+ <label>Timeout (s)</label>
579
+ <input type="range" id="timeout-slider" min="60" max="300" step="60" value="60">
580
+ <span class="slider-val" id="timeout-slider-val">60</span>
581
  </div>
582
  </div>
583
+ <div class="insp-section">
584
+ <button id="btn-run-video" class="btn-run">Segment Video</button>
585
+ <div class="run-hint">⌘/Ctrl + Enter</div>
586
+ </div>
587
  </div>
588
 
589
+ <!-- Click Page -->
590
+ <div class="insp-page" id="page-click">
591
+ <div class="insp-section">
592
+ <div class="insp-section-title"><span>Instructions</span></div>
593
+ <p style="font-size:12px;color:var(--muted);line-height:1.6;margin-bottom:0;">
594
+ Click on the image in the canvas to add foreground points.
595
+ The model will segment the object you click on.
596
+ </p>
597
+ </div>
598
+ <div class="insp-section">
599
+ <button id="btn-run-click" class="btn-run">Run Inference</button>
600
+ <button id="btn-clear-click" class="btn-secondary">Clear Points</button>
601
+ </div>
602
+ </div>
603
+
604
+ <!-- Examples Page -->
605
  <div class="insp-page" id="page-examples">
606
  <div class="examples-list" id="examples-scroll"></div>
607
  </div>
 
609
 
610
  <!-- ══ Status bar ══ -->
611
  <div class="statusbar">
612
+ <div class="sb-section" id="sb-image-count">No media</div>
613
  <div class="sb-right">
614
  <a class="sb-link" href="https://huggingface.co/facebook/sam3" target="_blank" rel="noopener">facebook/sam3</a>
615
  <span class="sb-pill" id="sb-status">Ready</span>
 
618
 
619
  </div>
620
 
621
+ <input id="custom-file-input" type="file" accept="image/*,video/*" style="display:none;" />
 
 
 
 
 
622
  <div class="drop-overlay" id="drop-overlay"><div class="drop-overlay-inner">Drop media to upload</div></div>
623
 
624
  <script type="module">
625
  import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
626
 
 
627
  const galleryGrid = document.getElementById('image-gallery-grid');
628
  const fileInput = document.getElementById('custom-file-input');
629
  const btnUpload = document.getElementById('tb-upload');
630
  const btnRemove = document.getElementById('tb-remove');
631
  const btnClear = document.getElementById('tb-clear');
632
+ const outBody = document.getElementById('output-image-container');
633
+ const outPh = document.getElementById('output-placeholder');
634
+ const dlBtn = document.getElementById('dl-btn-output');
 
 
 
 
 
 
635
  const sbStatus = document.getElementById('sb-status');
636
  const loader = document.getElementById('output-loader');
637
  const loaderText = document.getElementById('loader-text');
638
  const loaderFill = document.getElementById('loader-bar-fill');
 
 
 
 
639
  const historyStrip = document.getElementById('history-strip');
640
  const historyCount = document.getElementById('history-count');
 
 
 
 
 
 
641
  const dropOverlay = document.getElementById('drop-overlay');
642
  const canvasMeta = document.getElementById('canvas-meta');
643
+ const segResult = document.getElementById('seg-result');
644
+ const segInput = document.getElementById('seg-input');
645
  const connDot = document.getElementById('conn-dot');
646
  const connText = document.getElementById('conn-text');
647
 
648
+ const promptImage = document.getElementById('prompt-image');
649
+ const promptVideo = document.getElementById('prompt-video');
650
+ const btnRunImage = document.getElementById('btn-run-image');
651
+ const btnRunVideo = document.getElementById('btn-run-video');
652
+ const btnRunClick = document.getElementById('btn-run-click');
653
+ const btnClearClick= document.getElementById('btn-clear-click');
654
+
655
+ const confSlider = document.getElementById('conf-slider');
656
+ const confVal = document.getElementById('conf-slider-val');
657
+ const framesSlider = document.getElementById('frames-slider');
658
+ const framesVal = document.getElementById('frames-slider-val');
659
+ const timeoutSlider = document.getElementById('timeout-slider');
660
+ const timeoutVal = document.getElementById('timeout-slider-val');
661
+
662
+ let currentMedia = null; // {b64, name, type: 'image'|'video', naturalW, naturalH}
663
+ let currentResult = null; // {b64, type: 'image'|'video'}
664
  let history = [];
665
  let historyIdx = -1;
666
+ let viewMode = 'result';
667
+ let currentJob = null;
668
+ let running = false;
669
+ let currentTab = 'image';
670
  let clickPoints = [];
671
 
672
+ const SUGGESTIONS_IMG = [
673
+ ['Player', 'player in white'],
674
+ ['Ball', 'basketball'],
675
+ ['Referee', 'referee in black'],
 
676
  ];
677
 
 
678
  function showToast(message, type) {
679
  let toast = document.getElementById('app-toast');
680
  if (!toast) {
 
689
  toast.className = 'toast-notification ' + (type || 'error');
690
  icon.textContent = type === 'warning' ? '⚠' : type === 'info' ? 'ℹ' : '✗';
691
  text.textContent = message;
 
 
692
  toast.classList.add('visible');
693
+ setTimeout(() => toast.classList.remove('visible'), 3500);
 
 
 
 
 
 
694
  }
695
 
696
  function setStatus(text, cls) {
 
698
  sbStatus.className = 'sb-pill' + (cls ? ' ' + cls : '');
699
  }
700
 
701
+ function showLoader(text) {
702
+ loaderText.textContent = text || 'Processing…';
703
+ loaderFill.classList.remove('determinate');
704
+ loaderFill.style.width = '100%';
705
+ loader.classList.add('active');
706
+ setStatus('Processing…');
707
+ }
708
+ function hideLoader(statusText, cls) {
709
+ loader.classList.remove('active');
710
+ setStatus(statusText || 'Done', cls || 'done');
711
+ }
712
+
713
+ function getCanvasMedia() { return outBody.querySelector('img.modern-out-img, video.modern-out-vid'); }
714
+
715
  function renderCanvas() {
716
+ // Clear points if not click tab
717
+ if (currentTab !== 'click') {
718
+ outBody.querySelectorAll('.click-point').forEach(p => p.remove());
719
+ }
720
 
721
+ let media = getCanvasMedia();
722
+ const showResult = viewMode === 'result' && currentResult;
723
+ const showInput = viewMode === 'input' && currentMedia;
724
 
725
  if (showResult || showInput) {
726
  outPh.style.display = 'none';
727
+ const data = showResult ? currentResult : currentMedia;
728
 
729
+ if (data.type === 'video') {
730
+ if (!media || media.tagName !== 'VIDEO') {
731
+ if (media) media.remove();
732
+ media = document.createElement('video');
733
+ media.className = 'modern-out-vid';
734
+ media.controls = true; media.autoplay = true; media.loop = true; media.muted = true;
735
+ outBody.appendChild(media);
736
+ }
737
+ media.src = data.b64;
 
 
 
738
  } else {
739
+ if (!media || media.tagName !== 'IMG') {
740
+ if (media) media.remove();
741
+ media = document.createElement('img');
742
+ media.className = 'modern-out-img';
743
+ outBody.appendChild(media);
744
+ }
745
+ media.src = data.b64;
 
 
 
 
 
 
746
  }
747
+ canvasMeta.textContent = (showResult ? 'result' : 'input') + ' · ' + data.type;
748
  } else {
749
+ if (media) media.style.display = 'none';
750
  outPh.style.display = '';
751
  canvasMeta.textContent = 'No media loaded';
752
  }
 
754
  }
755
 
756
  function updateRailState() {
 
757
  const hasMedia = !!currentMedia;
758
+ const hasResult = !!currentResult;
 
759
  btnRemove.disabled = !hasMedia;
760
+ btnClear.disabled = !hasMedia && !hasResult;
761
+ dlBtn.disabled = !hasResult;
 
 
 
 
 
 
 
762
  }
763
 
764
+ function setViewMode(mode) {
765
+ viewMode = mode;
766
+ segResult.classList.toggle('active', mode === 'result');
767
+ segInput.classList.toggle('active', mode === 'input');
 
 
768
  renderCanvas();
769
  }
770
+ segResult.addEventListener('click', () => setViewMode('result'));
771
+ segInput.addEventListener('click', () => setViewMode('input'));
772
 
773
+ function setMedia(b64, name, type, naturalW=0, naturalH=0) {
774
+ currentMedia = { b64, name, type, naturalW, naturalH };
775
  currentResult = null;
 
776
  renderFilmstrip();
777
+ setViewMode('input');
 
778
  }
779
 
780
  function clearAll() {
 
782
  currentResult = null;
783
  clickPoints = [];
784
  renderFilmstrip();
 
785
  renderCanvas();
786
  }
787
 
 
795
  } else {
796
  const t = document.createElement('div');
797
  t.className = 'fs-thumb selected';
798
+ t.innerHTML = '<img src="' + currentMedia.b64 + '" alt="">' +
799
+ '<span class="fs-badge">' + currentMedia.type + '</span>';
800
+ t.addEventListener('click', () => setViewMode('input'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  galleryGrid.appendChild(t);
802
  }
803
  const add = document.createElement('div');
804
  add.className = 'fs-add';
805
+ add.innerHTML = '<span class="add-icon">+</span><span class="add-text">Add</span>';
806
  add.addEventListener('click', () => fileInput.click());
807
  galleryGrid.appendChild(add);
 
808
  }
809
 
810
+ function processFile(file) {
811
+ if (!file) return false;
 
 
 
 
 
 
812
  const reader = new FileReader();
813
+ const type = file.type.startsWith('video/') ? 'video' : 'image';
814
  reader.onload = (e) => {
 
 
815
  if (type === 'image') {
816
+ // Get natural dimensions for click mapping
817
+ const img = new Image();
818
+ img.onload = () => {
819
+ setMedia(e.target.result, file.name, 'image', img.naturalWidth, img.naturalHeight);
820
+ };
821
+ img.src = e.target.result;
822
  } else {
823
+ setMedia(e.target.result, file.name, 'video');
824
  }
 
825
  };
826
  reader.readAsDataURL(file);
827
+ return true;
828
  }
829
 
830
+ fileInput.addEventListener('change', (e) => { processFile(e.target.files[0]); e.target.value = ''; });
831
  btnUpload.addEventListener('click', () => fileInput.click());
832
+ btnRemove.addEventListener('click', clearAll);
833
+ btnClear.addEventListener('click', clearAll);
 
 
 
 
 
 
 
 
 
834
 
 
835
  let dragDepth = 0;
836
  window.addEventListener('dragenter', (e) => {
837
+ e.preventDefault(); dragDepth++; dropOverlay.classList.add('visible');
 
 
 
838
  });
839
  window.addEventListener('dragleave', (e) => {
840
+ e.preventDefault(); dragDepth = Math.max(0, dragDepth - 1);
 
841
  if (dragDepth === 0) dropOverlay.classList.remove('visible');
842
  });
843
  window.addEventListener('dragover', (e) => { e.preventDefault(); });
844
  window.addEventListener('drop', (e) => {
845
+ e.preventDefault(); dragDepth = 0; dropOverlay.classList.remove('visible');
 
 
846
  if (e.dataTransfer.files.length) {
847
+ if (processFile(e.dataTransfer.files[0])) showToast('Media loaded', 'info');
848
  }
849
  });
850
 
851
+ // Tabs Logic
852
+ const tabs = ['image', 'video', 'click', 'examples'];
853
+ tabs.forEach(tab => {
854
+ document.getElementById(`tab-${tab}`).addEventListener('click', () => {
855
+ currentTab = tab;
856
+ tabs.forEach(t => {
857
+ document.getElementById(`tab-${t}`).classList.toggle('active', t === tab);
858
+ const page = document.getElementById(`page-${t}`);
859
+ if (page) page.classList.toggle('active', t === tab);
860
+ });
861
+
862
+ // Adjust file input accept type
863
+ fileInput.accept = tab === 'video' ? 'video/*' : 'image/*';
864
+
865
+ // Clear points if leaving click tab
866
+ if (tab !== 'click') {
867
+ outBody.querySelectorAll('.click-point').forEach(p => p.remove());
868
+ clickPoints = [];
869
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
870
  });
 
871
  });
872
 
873
+ // Click Segmentation Logic
874
+ outBody.addEventListener('click', (e) => {
875
+ if (currentTab !== 'click' || !currentMedia || currentMedia.type !== 'image') return;
876
+ if (!e.target.classList.contains('modern-out-img')) return;
877
+
878
+ const rect = e.target.getBoundingClientRect();
879
+ const x = e.clientX - rect.left;
880
+ const y = e.clientY - rect.top;
881
+
882
+ const scaleX = currentMedia.naturalW / rect.width;
883
+ const scaleY = currentMedia.naturalH / rect.height;
884
+ const natX = Math.round(x * scaleX);
885
+ const natY = Math.round(y * scaleY);
886
+
887
+ clickPoints.push([natX, natY]);
888
+
889
+ const dot = document.createElement('div');
890
+ dot.className = 'click-point';
891
+ dot.style.left = x + 'px';
892
+ dot.style.top = y + 'px';
893
+ outBody.appendChild(dot);
894
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
895
 
896
+ btnClearClick.addEventListener('click', () => {
897
+ clickPoints = [];
898
+ outBody.querySelectorAll('.click-point').forEach(p => p.remove());
899
+ if (currentResult) {
900
+ currentResult = null;
901
+ setViewMode('input');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
902
  }
903
  });
904
 
905
+ // Sliders
906
+ confSlider.addEventListener('input', () => confVal.textContent = confSlider.value);
907
+ framesSlider.addEventListener('input', () => framesVal.textContent = framesSlider.value);
908
+ timeoutSlider.addEventListener('input', () => timeoutVal.textContent = timeoutSlider.value);
909
+
910
+ // Download
911
  dlBtn.addEventListener('click', () => {
912
+ if (!currentResult) return;
913
+ const a = document.createElement('a');
914
+ a.href = currentResult.b64;
915
+ a.download = `sam3_result_${Date.now()}.${currentResult.type === 'video' ? 'mp4' : 'png'}`;
916
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
 
917
  });
918
 
919
+ // Backend
920
  let client = null;
921
  try {
922
  client = await Client.connect(window.location.origin);
 
924
  connDot.classList.remove('off');
925
  connText.textContent = 'connected';
926
  } catch (e) {
 
927
  setStatus('Offline', 'error');
928
  connDot.classList.add('off');
929
  connText.textContent = 'offline';
930
+ showToast('Could not connect to backend', 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
931
  }
932
 
933
+ async function runJob(endpoint, payload) {
 
 
 
 
 
 
 
934
  if (running) return;
935
+ running = true;
 
 
936
  setRunningUI(true);
937
  showLoader('Submitting to queue…');
938
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
939
  try {
940
+ currentJob = client.submit(endpoint, payload);
941
  for await (const msg of currentJob) {
942
  if (msg.type === 'status') {
943
  if (msg.status === 'pending') {
944
+ setLoaderProgress('In queue position ' + (msg.position + 1), null);
 
 
 
945
  setStatus('Queued');
946
  } else if (msg.status === 'generating') {
947
  setStatus('Processing…');
948
+ setLoaderProgress('Processing…', null);
949
  } else if (msg.status === 'error') {
950
+ throw new Error(msg.message || 'Failed');
951
  }
952
  } else if (msg.type === 'data') {
953
+ const out = msg.data[0];
954
+ if (out.image) {
955
+ currentResult = { b64: out.image, type: 'image' };
 
 
 
 
956
  hideLoader('Done', 'done');
957
+ setViewMode('result');
958
+ showToast('Segmentation complete', 'info');
959
+ } else if (out.video) {
960
+ currentResult = { b64: out.video, type: 'video' };
961
  hideLoader('Done', 'done');
962
+ setViewMode('result');
963
+ showToast('Video processing complete', 'info');
964
  }
965
  }
966
  }
967
  } catch (e) {
 
968
  hideLoader('Error', 'error');
969
+ showToast(e.message || 'Generation failed', 'error');
 
970
  } finally {
971
  setRunningUI(false);
972
  currentJob = null;
973
  }
974
  }
975
 
976
+ function setRunningUI(on) {
977
+ running = on;
978
+ btnRunImage.disabled = on;
979
+ btnRunVideo.disabled = on;
980
+ btnRunClick.disabled = on;
981
+ btnRunImage.textContent = on ? 'Processing…' : 'Segment Image';
982
+ btnRunVideo.textContent = on ? 'Processing…' : 'Segment Video';
983
+ btnRunClick.textContent = on ? 'Processing…' : 'Run Inference';
984
+ }
985
 
986
+ function setLoaderProgress(text, frac) {
987
+ loaderText.textContent = text;
988
+ if (frac != null) {
989
+ loaderFill.classList.add('determinate');
990
+ loaderFill.style.width = (frac * 100) + '%';
991
+ }
992
+ }
993
 
994
+ btnRunImage.addEventListener('click', () => {
995
+ if (!currentMedia || currentMedia.type !== 'image') return showToast('Please upload an image', 'error');
996
+ const p = promptImage.value.trim();
997
+ if (!p) return showToast('Please enter a prompt', 'error');
998
+ runJob('/segment_image', {
999
+ image_b64: currentMedia.b64,
1000
+ prompt: p,
1001
+ conf_thresh: parseFloat(confSlider.value)
1002
+ });
1003
+ });
1004
 
1005
+ btnRunVideo.addEventListener('click', () => {
1006
+ if (!currentMedia || currentMedia.type !== 'video') return showToast('Please upload a video', 'error');
1007
+ const p = promptVideo.value.trim();
1008
+ if (!p) return showToast('Please enter a prompt', 'error');
1009
+ runJob('/segment_video', {
1010
+ video_b64: currentMedia.b64,
1011
+ prompt: p,
1012
+ frame_limit: parseInt(framesSlider.value),
1013
+ time_limit: parseInt(timeoutSlider.value)
1014
+ });
1015
+ });
1016
 
1017
+ btnRunClick.addEventListener('click', () => {
1018
+ if (!currentMedia || currentMedia.type !== 'image') return showToast('Please upload an image', 'error');
1019
+ if (clickPoints.length === 0) return showToast('Click on the image first', 'warning');
1020
+ runJob('/segment_click', {
1021
  image_b64: currentMedia.b64,
1022
+ points_json: JSON.stringify(clickPoints)
1023
+ });
1024
+ });
1025
 
1026
+ // Config
1027
+ function renderConfig(cfg) {
1028
+ const list = document.getElementById('examples-scroll');
1029
+ list.innerHTML = '';
1030
+ cfg.examples.forEach(ex => {
1031
+ const card = document.createElement('div');
1032
+ card.className = 'example-card';
1033
+ const thumbHtml = ex.thumb ? '<img src="' + ex.thumb + '" alt="">' : '<div class="example-thumb-placeholder">Preview</div>';
1034
+ card.innerHTML = '<div class="example-thumb-wrap">' + thumbHtml + '</div>' +
1035
+ '<div class="example-body"><div class="example-meta"><span class="example-lora-badge">' + ex.type + '</span></div>' +
1036
+ '<div class="example-prompt-text">' + ex.prompt + '</div></div>';
1037
+ card.addEventListener('click', async () => {
1038
+ card.classList.add('loading');
1039
+ try {
1040
+ const res = await client.predict('/load_example', { idx: ex.idx });
1041
+ const data = res.data[0];
1042
+ if (data.status === 'ok') {
1043
+ const img = new Image();
1044
+ img.onload = () => {
1045
+ setMedia(data.image, 'example.jpg', 'image', img.naturalWidth, img.naturalHeight);
1046
+ promptImage.value = data.prompt;
1047
+ document.getElementById('tab-image').click();
1048
+ };
1049
+ img.src = data.image;
1050
+ }
1051
+ } catch(e) {
1052
+ showToast('Failed to load example', 'error');
1053
+ } finally {
1054
+ card.classList.remove('loading');
1055
  }
1056
+ });
1057
+ list.appendChild(card);
1058
+ });
 
 
 
 
 
 
 
1059
  }
1060
 
1061
+ try {
1062
+ const cfg = await fetch('api/config').then(r => r.json());
1063
+ renderConfig(cfg);
1064
+ } catch (e) {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1065
 
1066
  renderFilmstrip();
 
 
1067
  renderCanvas();
1068
  </script>
1069
  </body>