@ -3,350 +3,312 @@
< head >
< meta charset = "UTF-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1" / >
< title > RMBG_SERVICE // BACKGROUND REMOVAL< / title >
< link rel = "stylesheet" href = "/styles.css" / >
< title > Background Removal & Segmentation< / title >
< style >
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background: #15171c; color: #e8e8ea; padding: 24px;
}
h1 { font-size: 1.25rem; font-weight: 600; margin: 0 0 4px; }
.sub { color: #8a8f99; font-size: .85rem; margin-bottom: 16px; }
.wrap { max-width: 1100px; margin: 0 auto; }
.tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid #2a2f3a; }
.tab { background: none; border: 0; color: #8a8f99; font-size: .9rem; font-weight: 600;
padding: 10px 16px; cursor: pointer; border-bottom: 2px solid transparent; }
.tab.active { color: #e8e8ea; border-bottom-color: #5b8cff; }
#drop {
border: 2px dashed #3a3f4b; border-radius: 12px; padding: 36px;
text-align: center; cursor: pointer; transition: border-color .15s, background .15s;
}
#drop.over { border-color: #5b8cff; background: #1c2230; }
#drop p { margin: 6px 0; color: #8a8f99; }
.controls { display: flex; gap: 12px; align-items: flex-end; margin: 14px 0; flex-wrap: wrap; }
.controls[hidden] { display: none; }
label.field { display: flex; flex-direction: column; gap: 4px; font-size: .72rem;
color: #8a8f99; text-transform: uppercase; letter-spacing: .04em; }
select, input[type=number], input[type=text] {
background: #2a2f3a; color: #e8e8ea; border: 1px solid #3a3f4b;
border-radius: 8px; padding: 8px 10px; font-size: .9rem;
}
input[type=number] { width: 78px; }
input[type=number]:disabled { opacity: .45; }
input[type=text]#prompt { width: 320px; }
.check { display: flex; align-items: center; gap: 6px; font-size: .85rem;
color: #e8e8ea; cursor: pointer; align-self: end; padding-bottom: 8px; }
.check input { width: 15px; height: 15px; accent-color: #5b8cff; cursor: pointer; }
/* help tooltips */
.help { display: inline-flex; align-items: center; justify-content: center;
width: 14px; height: 14px; margin-left: 5px; border-radius: 50%;
border: 1px solid #4a4f5b; color: #8a8f99; font-size: 9px; font-weight: 700;
font-style: normal; cursor: help; position: relative; vertical-align: middle; }
.help:hover { color: #e8e8ea; border-color: #5b8cff; }
.help:hover::after {
content: attr(data-tip); position: absolute; bottom: 150%; left: 50%;
transform: translateX(-50%); width: 220px; background: #0c0d11;
color: #d8d9dc; border: 1px solid #3a3f4b; border-radius: 6px;
padding: 7px 9px; font-size: .72rem; font-weight: 400; line-height: 1.4;
text-transform: none; letter-spacing: normal; white-space: normal;
z-index: 50; pointer-events: none; }
button.go {
background: #5b8cff; color: #fff; border: 0; border-radius: 8px;
padding: 10px 18px; font-size: .9rem; cursor: pointer; font-weight: 600;
}
button.go:disabled { background: #3a3f4b; cursor: not-allowed; }
button.ghost { background: #2a2f3a; color: #fff; border: 0; border-radius: 8px;
padding: 10px 18px; font-size: .9rem; cursor: pointer; font-weight: 600; }
.go-row { display: flex; gap: 12px; align-items: center; margin: 14px 0; flex-wrap: wrap; }
.status { color: #8a8f99; font-size: .85rem; }
.status.err { color: #ff6b6b; }
.hint { color: #6b7280; font-size: .78rem; margin: -4px 0 4px; }
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
.panel { background: #1c1f27; border-radius: 12px; padding: 12px; }
.panel h2 { font-size: .8rem; font-weight: 600; color: #8a8f99; margin: 0 0 8px;
text-transform: uppercase; letter-spacing: .05em; }
.imgbox {
min-height: 260px; display: flex; align-items: center; justify-content: center;
border-radius: 8px; overflow: hidden;
}
.checker {
background-image:
linear-gradient(45deg, #2a2f3a 25%, transparent 25%),
linear-gradient(-45deg, #2a2f3a 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #2a2f3a 75%),
linear-gradient(-45deg, transparent 75%, #2a2f3a 75%);
background-size: 22px 22px;
background-position: 0 0, 0 11px, 11px -11px, -11px 0;
background-color: #20242d;
}
.imgbox img { max-width: 100%; max-height: 70vh; display: block; }
.imgbox img[src] { cursor: zoom-in; }
@media (max-width: 720px) { .panels { grid-template-columns: 1fr; } }
/* lightbox */
.lightbox { position: fixed; inset: 0; z-index: 100; background: rgba(12,13,17,.97);
display: flex; align-items: center; justify-content: center; }
.lightbox[hidden] { display: none; }
.lb-stage { width: 100vw; height: 100vh; overflow: hidden;
display: flex; align-items: center; justify-content: center; }
.lb-stage img { max-width: 100vw; max-height: 100vh; transform-origin: 0 0;
cursor: grab; user-select: none; -webkit-user-drag: none; will-change: transform; }
.lb-stage.grabbing img { cursor: grabbing; }
.lb-bar { position: fixed; top: 0; left: 0; right: 0; padding: 14px 20px;
z-index: 2; display: flex; justify-content: space-between; align-items: center;
color: #8a8f99; font-size: .8rem; pointer-events: none; }
.lb-close { pointer-events: auto; background: #2a2f3a; color: #e8e8ea;
border: 1px solid #3a3f4b; border-radius: 8px; width: 34px; height: 34px;
font-size: 1rem; line-height: 1; padding: 0; cursor: pointer;
display: flex; align-items: center; justify-content: center; }
< / style >
< / head >
< body >
< div class = "container" id = "container" >
< div class = "wrap" >
< h1 > Background Removal & Segmentation< / h1 >
< div class = "sub" > Automatic removal, or prompt-conditioned segmentation.< / div >
< button class = "sidebar-toggle" id = "sidebarToggle" title = "Toggle panel" aria-label = "Toggle panel" > ‹ < / button >
< div class = "tabs" >
< button class = "tab active" data-tab = "auto" > Auto remove< / button >
< button class = "tab" data-tab = "prompt" > Prompt segment< / button >
< / div >
<!-- ============ CONTROL SIDEBAR ============ -->
< aside class = "controls panel" >
< div id = "drop" >
< p > < strong > Drop an image here< / strong > or click to choose< / p >
< p id = "fname" > No file selected< / p >
< input id = "file" type = "file" accept = "image/*" hidden / >
< / div >
< header class = "app-header" >
< div class = "app-title" > RMBG_SERVICE< / div >
< div class = "app-sub" > [ BACKGROUND REMOVAL // SEGMENTATION ]< / div >
< / header >
<!-- Auto (BiRefNet / RMBG - 2.0) controls -->
< div class = "controls" id = "ctl-auto" >
< label class = "field" > Model
< select id = "model" >
< option value = "general" > general — clean single subjects (fast)< / option >
< option value = "HR" selected > HR — large / detailed scenes< / option >
< option value = "portrait" > portrait — people< / option >
< option value = "matting" > matting — soft edges / hair< / option >
< option value = "lite" > lite — fastest< / option >
< option value = "rmbg2" > rmbg2 — BRIA RMBG-2.0< / option >
< / select >
< / label >
< label class = "field" > Resolution
< select id = "resolution" >
< option value = "1024" > 1024< / option >
< option value = "1536" > 1536< / option >
< option value = "2048" > 2048< / option >
< option value = "2560" selected > 2560< / option >
< / select >
< / label >
< / div >
<!-- upload -->
< section class = "upload-section" >
< div class = "upload-box" id = "drop" >
< input id = "file" type = "file" accept = "image/*" / >
< div class = "upload-text" > ▲ DROP IMAGE< / div >
< div class = "upload-hint" id = "fname" > NO FILE SELECTED< / div >
< img class = "preview-thumb" id = "thumb" alt = "" hidden / >
< / div >
< / section >
<!-- Prompt (GroundingDINO + SAM) controls -->
< div class = "controls" id = "ctl-prompt" hidden >
< label class = "field" > Prompt — what to keep
< input type = "text" id = "prompt" placeholder = "e.g. the dog · cow. person." / >
< / label >
< label class = "field" >
< span > Box threshold< span class = "help" data-tip = "Minimum confidence for GroundingDINO to keep a detected box. Lower finds more (and looser) objects; higher keeps only strong matches." > ?< / span > < / span >
< input type = "number" id = "boxThr" value = "0.3" min = "0" max = "1" step = "0.05" / >
< / label >
< label class = "field" >
< span > Text threshold< span class = "help" data-tip = "How strongly a detection must match your prompt words. Lower = looser word matching; higher = stricter." > ?< / span > < / span >
< input type = "number" id = "textThr" value = "0.25" min = "0" max = "1" step = "0.05" / >
< / label >
< / div >
<!-- mode -->
< div class = "control-group" >
< div class = "control-label" > // MODE< / div >
< div class = "radio-row" >
< label > < input type = "radio" name = "mode" value = "auto" checked / > AUTO REMOVE< / label >
< label > < input type = "radio" name = "mode" value = "prompt" / > PROMPT SEGMENT< / label >
< / div >
<!-- Shared output controls -->
< div class = "controls" >
< label class = "field" > Background
< select id = "background" >
< option value = "alpha" selected > transparent< / option >
< option value = "white" > white< / option >
< option value = "black" > black< / option >
< option value = "gray" > gray< / option >
< option value = "green" > green< / option >
< option value = "blue" > blue< / option >
< option value = "red" > red< / option >
< / select >
< / label >
< label class = "field" >
< span > Edge offset (px)< span class = "help" data-tip = "Grow (+) or shrink (− ) the cutout edge by N pixels. A small negative value trims a leftover background-colored fringe around hair or fur." > ?< / span > < / span >
< input type = "number" id = "maskOffset" value = "0" min = "-20" max = "20" step = "1" / >
< / label >
< label class = "field" >
< span > Feather (px)< span class = "help" data-tip = "Gaussian blur applied to the mask edge, in pixels. Softens the cutout for smoother compositing onto a new background." > ?< / span > < / span >
< input type = "number" id = "maskBlur" value = "0" min = "0" max = "64" step = "1" / >
< / label >
< label class = "check" > < input type = "checkbox" id = "crop" checked / > Crop to subject< / label >
< label class = "field" > Margin (in)
< input type = "number" id = "cropMargin" value = "0" min = "0" step = "0.1" / >
< / label >
< / div >
< div class = "hint" id = "hint" > < / div >
< div class = "go-row" >
< button class = "go" id = "go" disabled > Remove background< / button >
< a id = "dl" download = "cutout.png" > < button id = "dlbtn" class = "ghost" disabled > Download PNG< / button > < / a >
< span id = "status" class = "status" > < / span >
< / div >
< div class = "panels" >
< div class = "panel" >
< h2 > Original< / h2 >
< div class = "imgbox" > < img id = "src" alt = "" / > < / div >
< / div >
<!-- AUTO controls -->
< div class = "control-group" id = "ctl-auto" >
< div class = "control-label" > // MODEL< / div >
< div class = "select-wrap" >
< select id = "model" >
< option value = "general" > GENERAL — CLEAN SINGLE SUBJECTS (FAST)< / option >
< option value = "HR" selected > HR — LARGE / DETAILED SCENES< / option >
< option value = "portrait" > PORTRAIT — PEOPLE< / option >
< option value = "matting" > MATTING — SOFT EDGES / HAIR< / option >
< option value = "lite" > LITE — FASTEST< / option >
< option value = "rmbg2" > RMBG2 — BRIA RMBG-2.0< / option >
< / select >
< / div >
< div class = "control-label" > // RESOLUTION< / div >
< div class = "select-wrap" >
< select id = "resolution" >
< option value = "1024" > 1024< / option >
< option value = "1536" > 1536< / option >
< option value = "2048" > 2048< / option >
< option value = "2560" selected > 2560< / option >
< / select >
< / div >
< div class = "panel" >
< h2 > Result< / h2 >
< div class = "imgbox checker" > < img id = "out" alt = "" / > < / div >
< / div >
<!-- PROMPT controls -->
< div class = "control-group" id = "ctl-prompt" >
< div class = "control-label" > // PROMPT — WHAT TO KEEP< / div >
< input type = "text" id = "prompt" placeholder = "e.g. THE DOG . COW. PERSON." / >
< div class = "slider-container" >
< div class = "control-label" > BOX THRESHOLD
< span class = "help" data-tip = "Minimum confidence for GroundingDINO to keep a detected box. Lower finds more (and looser) objects; higher keeps only strong matches." > ?< / span >
< / div >
< input type = "range" class = "slider" id = "boxThr" min = "0" max = "1" step = "0.05" value = "0.3" / >
< div class = "slider-value" id = "boxThrVal" > 0.30< / div >
< / div >
< div class = "slider-container" >
< div class = "control-label" > TEXT THRESHOLD
< span class = "help" data-tip = "How strongly a detection must match your prompt words. Lower = looser word matching; higher = stricter." > ?< / span >
< / div >
< input type = "range" class = "slider" id = "textThr" min = "0" max = "1" step = "0.05" value = "0.25" / >
< div class = "slider-value" id = "textThrVal" > 0.25< / div >
< / div >
< / div >
<!-- OUTPUT drawer -->
< div class = "control-group drawer" id = "outputDrawer" >
< div class = "drawer-header" >
< span class = "drawer-caret" > ▸ < / span >
< span class = "section-title" > OUTPUT< / span >
< / div >
< div class = "drawer-content" >
< div class = "control-label" > // BACKGROUND< / div >
< div class = "select-wrap" >
< select id = "background" >
< option value = "alpha" selected > TRANSPARENT< / option >
< option value = "white" > WHITE< / option >
< option value = "black" > BLACK< / option >
< option value = "gray" > GRAY< / option >
< option value = "green" > GREEN< / option >
< option value = "blue" > BLUE< / option >
< option value = "red" > RED< / option >
< / select >
< / div >
< div class = "slider-container" >
< div class = "control-label" > EDGE OFFSET < span class = "unit" > PX< / span >
< span class = "help" data-tip = "Grow (+) or shrink (-) the cutout edge by N pixels. A small negative value trims a leftover background-colored fringe around hair or fur." > ?< / span >
< / div >
< input type = "range" class = "slider" id = "maskOffset" min = "-20" max = "20" step = "1" value = "0" / >
< div class = "slider-value" id = "maskOffsetVal" > 0< / div >
< / div >
< div class = "slider-container" >
< div class = "control-label" > FEATHER < span class = "unit" > PX< / span >
< span class = "help" data-tip = "Gaussian blur applied to the mask edge, in pixels. Softens the cutout for smoother compositing onto a new background." > ?< / span >
< / div >
< input type = "range" class = "slider" id = "maskBlur" min = "0" max = "64" step = "1" value = "0" / >
< div class = "slider-value" id = "maskBlurVal" > 0< / div >
< / div >
< label class = "check-row" > < input type = "checkbox" id = "crop" checked / > CROP TO SUBJECT< / label >
< div class = "slider-container" id = "cropMarginWrap" >
< div class = "control-label" > CROP MARGIN < span class = "unit" > IN< / span > < / div >
< input type = "range" class = "slider" id = "cropMargin" min = "0" max = "5" step = "0.1" value = "0" / >
< div class = "slider-value" id = "cropMarginVal" > 0.0 IN< / div >
< / div >
< / div >
< / div >
<!-- actions -->
< div class = "action-row" >
< button class = "action-btn primary" id = "go" disabled > REMOVE BACKGROUND< / button >
< a id = "dl" download = "cutout.png" > < button class = "action-btn" id = "dlbtn" disabled > ▼ DOWNLOAD PNG< / button > < / a >
< / div >
< button class = "reset-btn" id = "reset" > ✕ CLEAR< / button >
< div class = "status" id = "status" > < / div >
< div class = "hint" id = "hint" > < / div >
< / aside >
<!-- ============ PREVIEW ============ -->
< main class = "preview-container panel" >
< div class = "preview-tabs" >
< button class = "preview-tab active" id = "tabOriginal" data-view = "original" disabled > ORIGINAL< / button >
< button class = "preview-tab" id = "tabResult" data-view = "result" disabled > RESULT< / button >
< / div >
< div class = "preview-stage" >
< div class = "no-preview" id = "noPreview" > [ NO IMAGE LOADED ]< / div >
< img class = "preview-canvas" id = "src" alt = "" hidden / >
< img class = "preview-canvas checker" id = "out" alt = "" hidden / >
< / div >
< / main >
< / div >
< / div >
<!-- lightbox -->
< div id = "lightbox" class = "lightbox" hidden >
< div class = "lb-bar" >
< span > SCROLL TO ZOOM · DRAG TO PAN · DOUBLE-CLICK RESETS · ESC CLOSES < / span >
< button class = "lb-close" id = "lbClose" title = "Close" > ✕ < / button >
< span > scroll to zoom · drag to pan · double-click resets · Esc closes< / span >
< button class = "lb-close" id = "lbClose" title = "Close" > ✕< / button >
< / div >
< div class = "lb-stage" id = "lbStage" > < img id = "lbImg" alt = "" / > < / div >
< / div >
< script >
const $ = id => document.getElementById(id);
const drop = document.getElementById('drop');
const fileInput = document.getElementById('file');
const fname = document.getElementById('fname');
const go = document.getElementById('go');
const dl = document.getElementById('dl');
const dlbtn = document.getElementById('dlbtn');
const statusEl = document.getElementById('status');
const srcImg = document.getElementById('src');
const outImg = document.getElementById('out');
const hint = document.getElementById('hint');
const container = $('container');
const sidebarToggle = $('sidebarToggle');
const drop = $('drop');
const fileInput = $('file');
const fname = $('fname');
const thumb = $('thumb');
const go = $('go');
const dl = $('dl');
const dlbtn = $('dlbtn');
const statusEl = $('status');
const hint = $('hint');
const reset = $('reset');
const srcImg = $('src');
const outImg = $('out');
const noPreview = $('noPreview');
const modelSel = document.getElementById('model');
const resSel = document.getElementById('resolution');
const promptInput = document.getElementById('prompt');
const boxThr = document.getElementById('boxThr');
const textThr = document.getElementById('textThr');
const bgSel = document.getElementById('background');
const maskOffset = document.getElementById('maskOffset');
const maskBlur = document.getElementById('maskBlur');
const cropChk = document.getElementById('crop');
const cropMargin = document.getElementById('cropMargin');
const modelSel = $('model');
const resSel = $('resolution');
const promptInput= $('prompt');
const boxThr = $('boxThr');
const textThr = $('textThr');
const bgSel = $('background');
const maskOffset = $('maskOffset');
const maskBlur = $('maskBlur');
const cropChk = $('crop');
const cropMargin = $('cropMargin');
const cropMarginWrap = $('cropMarginWrap');
const ctlAuto = $('ctl-auto');
const ctlPrompt = $('ctl-prompt');
const tabOriginal= $('tabOriginal');
const tabResult = $('tabResult');
const ctlAuto = document.getElementById('ctl-auto');
const ctlPrompt = document.getElementById('ctl-prompt');
let selectedFile = null;
let mode = 'auto';
let tab = 'auto';
const HINTS = {
auto: 'Large or busy scenes segment best with HR at 2048+. The general model expects a clear single subject at 1024.',
auto: 'Large or busy scenes segment best with HR at 2048+. The general model expects a clear single subject at 1024.',
prompt: 'Type what to keep, e.g. "the dog" (or several: "cow. person."). Lower the box threshold to detect more / fainter objects.',
};
/* ---- mode ---- */
function setGroupEnabled(group, enabled) {
group.classList.toggle('disabled', !enabled);
group.querySelectorAll('input, select').forEach(el => { el.disabled = !enabled; });
function setTab(name) {
tab = name;
document.querySelectorAll('.tab').forEach(t =>
t.classList.toggle('active', t.dataset.tab === name));
ctlAuto.hidden = name !== 'auto';
ctlPrompt.hidden = name !== 'prompt';
go.textContent = name === 'auto' ? 'Remove background' : 'Segment';
hint.textContent = HINTS[name];
}
function setMode(m) {
mode = m;
setGroupEnabled(ctlAuto, m === 'auto');
setGroupEnabled(ctlPrompt, m === 'prompt');
go.textContent = m === 'auto' ? 'REMOVE BACKGROUND' : 'SEGMENT';
hint.textContent = HINTS[m];
}
document.querySelectorAll('input[name="mode"]').forEach(r =>
r.addEventListener('change', () => setMode(r.value)));
document.querySelectorAll('.tab').forEach(t =>
t.addEventListener('click', () => setTab(t.dataset.tab)));
cropChk.addEventListener('change', () => { cropMargin.disabled = !cropChk.checked; });
/* ---- drawers ---- */
document.querySelectorAll('.drawer-header').forEach(h =>
h.addEventListener('click', () => h.closest('.drawer').classList.toggle('open')));
/* ---- sidebar toggle (button + 'b' key) ---- */
function toggleSidebar() {
const collapsed = container.classList.toggle('sidebar-collapsed');
sidebarToggle.innerHTML = collapsed ? '› ' : '‹ ';
}
sidebarToggle.addEventListener('click', toggleSidebar);
/* ---- sliders ---- */
function wireSlider(input, out, fmt) {
const update = () => { out.textContent = fmt(input.value); };
input.addEventListener('input', update);
update();
}
wireSlider(boxThr, $('boxThrVal'), v => (+v).toFixed(2));
wireSlider(textThr, $('textThrVal'), v => (+v).toFixed(2));
wireSlider(maskOffset, $('maskOffsetVal'), v => (v > 0 ? '+' : '') + v);
wireSlider(maskBlur, $('maskBlurVal'), v => String(v));
wireSlider(cropMargin, $('cropMarginVal'), v => (+v).toFixed(1) + ' IN');
function syncCrop() {
cropMarginWrap.classList.toggle('disabled', !cropChk.checked);
}
cropChk.addEventListener('change', syncCrop);
/* ---- status ---- */
function setStatus(msg, isErr) {
statusEl.textContent = msg;
statusEl.className = 'status' + (isErr ? ' err' : '');
}
/* ---- preview tabs ---- */
function refreshTabs() {
const hasSrc = !!srcImg.getAttribute('src');
const hasOut = !!outImg.getAttribute('src');
tabOriginal.disabled = !hasSrc;
tabResult.disabled = !hasOut;
noPreview.hidden = hasSrc || hasOut;
}
function showView(v) {
if ((v === 'original' & & tabOriginal.disabled) ||
(v === 'result' & & tabResult.disabled)) return;
tabOriginal.classList.toggle('active', v === 'original');
tabResult.classList.toggle('active', v === 'result');
srcImg.hidden = v !== 'original';
outImg.hidden = v !== 'result';
}
document.querySelectorAll('.preview-tab').forEach(t =>
t.addEventListener('click', () => showView(t.dataset.view)));
/* ---- file handling ---- */
function pickFile(file) {
if (!file || !file.type.startsWith('image/')) {
setStatus('PLEASE CHOOSE AN IMAGE FILE .', true);
setStatus('Please choose an image file.', true);
return;
}
selectedFile = file;
const url = URL.createObjectURL(file);
const kb = Math.round(file.size / 1024);
fname.textContent = file.name + ' · ' + kb + ' KB';
thumb.src = url;
thumb.hidden = false;
drop.classList.add('has-file');
srcImg.onload = () => {
fname.textContent = file.name + ' · ' +
srcImg.naturalWidth + '× ' + srcImg.naturalHeight + ' PX · ' + kb + ' KB';
};
srcImg.src = url;
fname.textContent = file.name + ' (' + Math.round(file.size / 1024) + ' KB)';
srcImg.src = URL.createObjectURL(file);
outImg.removeAttribute('src');
dlbtn.disabled = true;
go.disabled = false;
setStatus('');
refreshTabs();
showView('original');
}
drop.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', e => pickFile(e.target.files[0]));
['dragenter', 'dragover'].forEach(ev =>
drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.add('drag over'); }));
drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.add('over'); }));
['dragleave', 'drop'].forEach(ev =>
drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('drag over'); }));
drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('over'); }));
drop.addEventListener('drop', e => pickFile(e.dataTransfer.files[0]));
reset.addEventListener('click', () => {
selectedFile = null;
fileInput.value = '';
fname.textContent = 'NO FILE SELECTED';
thumb.removeAttribute('src');
thumb.hidden = true;
drop.classList.remove('has-file');
srcImg.removeAttribute('src'); srcImg.hidden = true;
outImg.removeAttribute('src'); outImg.hidden = true;
go.disabled = true;
dlbtn.disabled = true;
setStatus('');
refreshTabs();
tabOriginal.classList.add('active');
tabResult.classList.remove('active');
});
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const r = new FileReader();
r.onload = () => resolve(r.result.split(',')[1]);
r.onload = () => resolve(r.result.split(',')[1]); // strip data URL prefix
r.onerror = reject;
r.readAsDataURL(file);
});
}
/* ---- lightbox ---- */
const lightbox = $ ('lightbox');
const lbStage = $ ('lbStage');
const lbImg = $ ('lbImg');
const lbClose = $ ('lbClose');
// --- lightbox: click to inspect, scroll to zoom, drag to pan ---
const lightbox = document.getElementById('lightbox');
const lbStage = document.getElementById('lbStage');
const lbImg = document.getElementById('lbImg');
const lbClose = document.getElementById('lbClose');
let lbScale = 1, lbTx = 0, lbTy = 0, lbDrag = null;
function lbApply() { lbImg.style.transform = `translate(${lbTx}px, ${lbTy}px) scale(${lbScale})`; }
function lbApply() {
lbImg.style.transform = `translate(${lbTx}px, ${lbTy}px) scale(${lbScale})`;
}
function lbReset() { lbScale = 1; lbTx = 0; lbTy = 0; lbApply(); }
function openLightbox(src, isResult) {
@ -366,11 +328,6 @@ lightbox.addEventListener('mousedown', e => {
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' & & !lightbox.hidden) closeLightbox();
if ((e.key === 'b' || e.key === 'B') & & !e.metaKey & & !e.ctrlKey & & !e.altKey) {
const t = e.target;
if (t & & (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return;
toggleSidebar();
}
});
lbStage.addEventListener('wheel', e => {
@ -404,16 +361,15 @@ window.addEventListener('mouseup', () => {
});
lbImg.addEventListener('dblclick', e => { e.preventDefault(); lbReset(); });
/* ---- run ---- */
go.addEventListener('click', async () => {
if (!selectedFile) return;
if (mode === 'prompt' & & !promptInput.value.trim()) {
setStatus('ENTER A PROMPT DESCRIBING WHAT TO KEEP .', true);
if (tab === 'prompt' & & !promptInput.value.trim()) {
setStatus('Enter a prompt describing what to keep .', true);
return;
}
go.disabled = true;
dlbtn.disabled = true;
setStatus('PROCESSING… (FIRST USE OF A MODEL DOWNLOADS ITS WEIGHTS )');
setStatus('Processing… (first use of a model downloads its weights )');
const t0 = performance.now();
try {
const b64 = await fileToBase64(selectedFile);
@ -426,7 +382,7 @@ go.addEventListener('click', async () => {
crop_margin: parseFloat(cropMargin.value) || 0,
};
let endpoint, body;
if (mode === 'auto') {
if (tab === 'auto') {
endpoint = '/predict';
body = { ...shared, model: modelSel.value, resolution: parseInt(resSel.value, 10) };
} else {
@ -447,16 +403,14 @@ go.addEventListener('click', async () => {
dl.href = dataUrl;
dl.download = selectedFile.name.replace(/\.[^.]+$/, '') + '.png';
dlbtn.disabled = false;
refreshTabs();
showView('result');
const secs = ((performance.now() - t0) / 1000).toFixed(1);
if (mode === 'auto') {
setStatus(`DONE — ${data.width}× ${data.height} · ${data.model} @ ${data.resolution} · ${secs}S `);
if (tab === 'auto') {
setStatus(`Done — ${data.width}× ${data.height} · ${data.model} @ ${data.resolution} · ${secs}s `);
} else {
const n = data.detections;
setStatus(`DONE — ${n} OBJECT${n === 1 ? '' : 'S'} MATCHED "${data.prompt}" · ` +
`${data.width}× ${data.height} · ${secs}S ` +
(n === 0 ? ' (TRY A LOWER BOX THRESHOLD )' : ''));
setStatus(`Done — ${n} object${n === 1 ? '' : 's'} matched "${data.prompt}" · ` +
`${data.width}× ${data.height} · ${secs}s ` +
(n === 0 ? ' (try a lower box threshold )' : ''));
}
} catch (err) {
setStatus(err.message || String(err), true);
@ -465,10 +419,7 @@ go.addEventListener('click', async () => {
}
});
/* ---- init ---- */
setMode('auto');
syncCrop();
refreshTabs();
setTab('auto');
< / script >
< / body >
< / html >