refactor styling: terminal

This commit is contained in:
Michael Pilosov 2026-05-16 18:22:26 -06:00
parent 27d9494123
commit 15b23c37c1
3 changed files with 677 additions and 259 deletions

View File

@ -41,13 +41,15 @@ from pathlib import Path
import litserve as ls
from fastapi import HTTPException
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, Response
from PIL import Image, ImageOps
from .model import BiRefNetService
from .prompt_segment import PromptSegmenter
_UI_HTML = (Path(__file__).parent / "static" / "index.html").read_text(encoding="utf-8")
_STATIC = Path(__file__).parent / "static"
_UI_HTML = (_STATIC / "index.html").read_text(encoding="utf-8")
_UI_CSS = (_STATIC / "styles.css").read_text(encoding="utf-8")
# Lazily-created prompt segmenter (DINO + SAM), shared by the /segment route.
_segmenter: PromptSegmenter | None = None
@ -132,6 +134,10 @@ def run() -> None:
def index() -> str:
return _UI_HTML
@server.app.get("/styles.css")
def styles() -> Response:
return Response(_UI_CSS, media_type="text/css")
@server.app.post("/segment")
def segment(payload: dict) -> dict:
"""Prompt-conditioned segmentation (GroundingDINO + SAM)."""

View File

@ -3,312 +3,350 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Background Removal &amp; 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>
<title>RMBG_SERVICE // BACKGROUND REMOVAL</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<div class="wrap">
<h1>Background Removal &amp; Segmentation</h1>
<div class="sub">Automatic removal, or prompt-conditioned segmentation.</div>
<div class="container" id="container">
<div class="tabs">
<button class="tab active" data-tab="auto">Auto remove</button>
<button class="tab" data-tab="prompt">Prompt segment</button>
</div>
<button class="sidebar-toggle" id="sidebarToggle" title="Toggle panel" aria-label="Toggle panel">&#8249;</button>
<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>
<!-- ============ CONTROL SIDEBAR ============ -->
<aside class="controls panel">
<!-- 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>
<header class="app-header">
<div class="app-title">RMBG_SERVICE</div>
<div class="app-sub">[ BACKGROUND REMOVAL // SEGMENTATION ]</div>
</header>
<!-- 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>
<!-- upload -->
<section class="upload-section">
<div class="upload-box" id="drop">
<input id="file" type="file" accept="image/*" />
<div class="upload-text">&#9650; DROP IMAGE</div>
<div class="upload-hint" id="fname">NO FILE SELECTED</div>
<img class="preview-thumb" id="thumb" alt="" hidden />
</div>
</section>
<!-- 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>
<!-- 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>
</div>
<div class="panel">
<h2>Result</h2>
<div class="imgbox checker"><img id="out" alt="" /></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 &mdash; CLEAN SINGLE SUBJECTS (FAST)</option>
<option value="HR" selected>HR &mdash; LARGE / DETAILED SCENES</option>
<option value="portrait">PORTRAIT &mdash; PEOPLE</option>
<option value="matting">MATTING &mdash; SOFT EDGES / HAIR</option>
<option value="lite">LITE &mdash; FASTEST</option>
<option value="rmbg2">RMBG2 &mdash; 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>
</div>
<!-- PROMPT controls -->
<div class="control-group" id="ctl-prompt">
<div class="control-label">// PROMPT &mdash; 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">&#9656;</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>&#9660; DOWNLOAD PNG</button></a>
</div>
<button class="reset-btn" id="reset">&#10005; 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>
<!-- 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 &middot; DRAG TO PAN &middot; DOUBLE-CLICK RESETS &middot; ESC CLOSES</span>
<button class="lb-close" id="lbClose" title="Close">&#10005;</button>
</div>
<div class="lb-stage" id="lbStage"><img id="lbImg" alt="" /></div>
</div>
<script>
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 $ = id => document.getElementById(id);
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 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 ctlAuto = document.getElementById('ctl-auto');
const ctlPrompt = document.getElementById('ctl-prompt');
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');
let selectedFile = null;
let tab = 'auto';
let mode = '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.',
};
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];
/* ---- mode ---- */
function setGroupEnabled(group, enabled) {
group.classList.toggle('disabled', !enabled);
group.querySelectorAll('input, select').forEach(el => { el.disabled = !enabled; });
}
document.querySelectorAll('.tab').forEach(t =>
t.addEventListener('click', () => setTab(t.dataset.tab)));
cropChk.addEventListener('change', () => { cropMargin.disabled = !cropChk.checked; });
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)));
/* ---- 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 ? '&#8250;' : '&#8249;';
}
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;
fname.textContent = file.name + ' (' + Math.round(file.size / 1024) + ' KB)';
srcImg.src = URL.createObjectURL(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;
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('over'); }));
drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.add('dragover'); }));
['dragleave', 'drop'].forEach(ev =>
drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('over'); }));
drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('dragover'); }));
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]); // strip data URL prefix
r.onload = () => resolve(r.result.split(',')[1]);
r.onerror = reject;
r.readAsDataURL(file);
});
}
// --- 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');
/* ---- lightbox ---- */
const lightbox = $('lightbox');
const lbStage = $('lbStage');
const lbImg = $('lbImg');
const lbClose = $('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) {
@ -328,6 +366,11 @@ 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 => {
@ -361,15 +404,16 @@ window.addEventListener('mouseup', () => {
});
lbImg.addEventListener('dblclick', e => { e.preventDefault(); lbReset(); });
/* ---- run ---- */
go.addEventListener('click', async () => {
if (!selectedFile) return;
if (tab === 'prompt' && !promptInput.value.trim()) {
setStatus('Enter a prompt describing what to keep.', true);
if (mode === '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);
@ -382,7 +426,7 @@ go.addEventListener('click', async () => {
crop_margin: parseFloat(cropMargin.value) || 0,
};
let endpoint, body;
if (tab === 'auto') {
if (mode === 'auto') {
endpoint = '/predict';
body = { ...shared, model: modelSel.value, resolution: parseInt(resSel.value, 10) };
} else {
@ -403,14 +447,16 @@ 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 (tab === 'auto') {
setStatus(`Done — ${data.width}×${data.height} · ${data.model} @ ${data.resolution} · ${secs}s`);
if (mode === '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);
@ -419,7 +465,10 @@ go.addEventListener('click', async () => {
}
});
setTab('auto');
/* ---- init ---- */
setMode('auto');
syncCrop();
refreshTabs();
</script>
</body>
</html>

View File

@ -652,5 +652,368 @@ label {
/* Rotate caret when drawer is open */
.drawer.open .drawer-caret {
transform: rotate(120deg);
transform: rotate(90deg);
}
/* ============================================================
APP-SPECIFIC RMBG_SERVICE background-removal UI.
Extends the brutalist terminal system above; same palette:
#000 ground, #8fbc8f primary, #7aa87a dim, #ff6b6b alert.
============================================================ */
/* --- sidebar header --- */
.app-header {
border-bottom: 2px solid #8fbc8f;
padding-bottom: 12px;
}
.app-title {
font-size: 22px;
font-weight: bold;
letter-spacing: 3px;
color: #8fbc8f;
}
.app-sub {
font-size: 10px;
letter-spacing: 1px;
color: #7aa87a;
margin-top: 6px;
}
/* --- selects (matches brutalist text/number inputs) --- */
.select-wrap {
position: relative;
width: 100%;
}
.select-wrap::after {
content: "\25BC";
position: absolute;
right: 0;
top: 1px;
bottom: 1px;
width: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
color: #000000;
background: #8fbc8f;
pointer-events: none;
}
select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: #000000;
border: 1px solid #8fbc8f;
color: #8fbc8f;
padding: 6px 28px 6px 8px;
font-family: inherit;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
width: 100%;
cursor: pointer;
border-radius: 0;
}
select:hover {
background: #0a0a0a;
}
select:focus {
outline: 2px solid #8fbc8f;
}
select option {
background: #000000;
color: #8fbc8f;
}
/* --- radio / checkbox rows --- */
.radio-row {
display: flex;
flex-direction: column;
gap: 8px;
}
.radio-row label,
.check-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: bold;
}
/* --- slider label units / state --- */
.slider-container .control-label {
display: flex;
align-items: baseline;
gap: 6px;
}
.unit {
color: #7aa87a;
font-size: 11px;
font-weight: normal;
}
.slider-container.disabled,
.control-group.disabled {
opacity: 0.3;
pointer-events: none;
}
/* breathing room between stacked controls inside the OUTPUT drawer */
.drawer-content {
display: flex;
flex-direction: column;
gap: 14px;
}
/* --- help tooltip (carried over, restyled brutalist) --- */
.help {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
margin-left: auto;
border: 1px solid #8fbc8f;
color: #8fbc8f;
font-size: 9px;
font-weight: bold;
cursor: help;
position: relative;
flex-shrink: 0;
}
.help:hover {
background: #8fbc8f;
color: #000000;
}
.help:hover::after {
content: attr(data-tip);
position: absolute;
bottom: 150%;
right: 0;
width: 220px;
background: #000000;
color: #8fbc8f;
border: 1px solid #8fbc8f;
padding: 8px 10px;
font-size: 11px;
font-weight: normal;
line-height: 1.5;
text-transform: none;
z-index: 200;
pointer-events: none;
}
/* --- action buttons --- */
.action-row {
display: flex;
flex-direction: column;
gap: 8px;
}
.action-btn {
width: 100%;
text-align: center;
}
.action-btn.primary {
background: #8fbc8f;
color: #000000;
}
.action-btn.primary:hover {
background: #7aa87a;
}
.action-btn:disabled {
border-color: #444;
color: #555;
background: #000000;
cursor: not-allowed;
}
#dl {
display: block;
text-decoration: none;
}
.reset-btn {
width: 100%;
text-align: center;
}
/* --- status / hint --- */
.status {
font-size: 12px;
color: #7aa87a;
min-height: 16px;
line-height: 1.5;
word-break: break-word;
}
.status.err {
color: #ff6b6b;
}
.hint {
font-size: 11px;
color: #5a7a5a;
line-height: 1.6;
text-transform: none;
border-left: 2px solid #2e3e2e;
padding-left: 8px;
}
/* --- preview area --- */
.preview-tabs {
position: absolute;
top: 12px;
left: 12px;
display: flex;
z-index: 10;
}
.preview-tab {
background: #000000;
border: 1px solid #8fbc8f;
color: #7aa87a;
padding: 6px 16px;
font-size: 11px;
cursor: pointer;
}
.preview-tab+.preview-tab {
border-left: none;
}
.preview-tab.active {
background: #8fbc8f;
color: #000000;
}
.preview-tab:disabled {
border-color: #444;
color: #444;
cursor: not-allowed;
}
.preview-stage {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.preview-canvas {
cursor: zoom-in;
}
.no-preview {
letter-spacing: 2px;
}
/* checkerboard behind transparent result pixels — green-on-black */
.preview-canvas.checker,
.lb-stage img.checker {
background-image:
linear-gradient(45deg, #1a2a1a 25%, transparent 25%),
linear-gradient(-45deg, #1a2a1a 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #1a2a1a 75%),
linear-gradient(-45deg, transparent 75%, #1a2a1a 75%);
background-size: 22px 22px;
background-position: 0 0, 0 11px, 11px -11px, -11px 0;
background-color: #000000;
}
/* upload box thumbnail state */
.upload-box.has-file {
border-color: #7aa87a;
}
.upload-box.has-file .upload-text {
color: #7aa87a;
}
/* --- lightbox --- */
.lightbox {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.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: #7aa87a;
font-size: 11px;
letter-spacing: 1px;
pointer-events: none;
}
.lb-close {
pointer-events: auto;
background: #8fbc8f;
color: #000000;
border: 2px solid #8fbc8f;
width: 32px;
height: 32px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.lb-close:hover {
background: #7aa87a;
}