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 import litserve as ls
from fastapi import HTTPException from fastapi import HTTPException
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, Response
from PIL import Image, ImageOps from PIL import Image, ImageOps
from .model import BiRefNetService from .model import BiRefNetService
from .prompt_segment import PromptSegmenter 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. # Lazily-created prompt segmenter (DINO + SAM), shared by the /segment route.
_segmenter: PromptSegmenter | None = None _segmenter: PromptSegmenter | None = None
@ -132,6 +134,10 @@ def run() -> None:
def index() -> str: def index() -> str:
return _UI_HTML return _UI_HTML
@server.app.get("/styles.css")
def styles() -> Response:
return Response(_UI_CSS, media_type="text/css")
@server.app.post("/segment") @server.app.post("/segment")
def segment(payload: dict) -> dict: def segment(payload: dict) -> dict:
"""Prompt-conditioned segmentation (GroundingDINO + SAM).""" """Prompt-conditioned segmentation (GroundingDINO + SAM)."""

View File

@ -3,312 +3,350 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Background Removal &amp; Segmentation</title> <title>RMBG_SERVICE // BACKGROUND REMOVAL</title>
<style> <link rel="stylesheet" href="/styles.css" />
: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> </head>
<body> <body>
<div class="wrap"> <div class="container" id="container">
<h1>Background Removal &amp; Segmentation</h1>
<div class="sub">Automatic removal, or prompt-conditioned segmentation.</div>
<div class="tabs"> <button class="sidebar-toggle" id="sidebarToggle" title="Toggle panel" aria-label="Toggle panel">&#8249;</button>
<button class="tab active" data-tab="auto">Auto remove</button>
<button class="tab" data-tab="prompt">Prompt segment</button>
</div>
<div id="drop"> <!-- ============ CONTROL SIDEBAR ============ -->
<p><strong>Drop an image here</strong> or click to choose</p> <aside class="controls panel">
<p id="fname">No file selected</p>
<input id="file" type="file" accept="image/*" hidden />
</div>
<!-- Auto (BiRefNet / RMBG-2.0) controls --> <header class="app-header">
<div class="controls" id="ctl-auto"> <div class="app-title">RMBG_SERVICE</div>
<label class="field">Model <div class="app-sub">[ BACKGROUND REMOVAL // SEGMENTATION ]</div>
<select id="model"> </header>
<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>
<!-- Prompt (GroundingDINO + SAM) controls --> <!-- upload -->
<div class="controls" id="ctl-prompt" hidden> <section class="upload-section">
<label class="field">Prompt — what to keep <div class="upload-box" id="drop">
<input type="text" id="prompt" placeholder="e.g. the dog · cow. person." /> <input id="file" type="file" accept="image/*" />
</label> <div class="upload-text">&#9650; DROP IMAGE</div>
<label class="field"> <div class="upload-hint" id="fname">NO FILE SELECTED</div>
<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> <img class="preview-thumb" id="thumb" alt="" hidden />
<input type="number" id="boxThr" value="0.3" min="0" max="1" step="0.05" /> </div>
</label> </section>
<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>
<!-- Shared output controls --> <!-- mode -->
<div class="controls"> <div class="control-group">
<label class="field">Background <div class="control-label">// MODE</div>
<select id="background"> <div class="radio-row">
<option value="alpha" selected>transparent</option> <label><input type="radio" name="mode" value="auto" checked /> AUTO REMOVE</label>
<option value="white">white</option> <label><input type="radio" name="mode" value="prompt" /> PROMPT SEGMENT</label>
<option value="black">black</option> </div>
<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> </div>
<div class="panel">
<h2>Result</h2> <!-- AUTO controls -->
<div class="imgbox checker"><img id="out" alt="" /></div> <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>
</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> </div>
<!-- lightbox -->
<div id="lightbox" class="lightbox" hidden> <div id="lightbox" class="lightbox" hidden>
<div class="lb-bar"> <div class="lb-bar">
<span>scroll to zoom · drag to pan · double-click resets · Esc closes</span> <span>SCROLL TO ZOOM &middot; DRAG TO PAN &middot; DOUBLE-CLICK RESETS &middot; ESC CLOSES</span>
<button class="lb-close" id="lbClose" title="Close"></button> <button class="lb-close" id="lbClose" title="Close">&#10005;</button>
</div> </div>
<div class="lb-stage" id="lbStage"><img id="lbImg" alt="" /></div> <div class="lb-stage" id="lbStage"><img id="lbImg" alt="" /></div>
</div> </div>
<script> <script>
const drop = document.getElementById('drop'); const $ = id => document.getElementById(id);
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 modelSel = document.getElementById('model'); const container = $('container');
const resSel = document.getElementById('resolution'); const sidebarToggle = $('sidebarToggle');
const promptInput = document.getElementById('prompt'); const drop = $('drop');
const boxThr = document.getElementById('boxThr'); const fileInput = $('file');
const textThr = document.getElementById('textThr'); const fname = $('fname');
const bgSel = document.getElementById('background'); const thumb = $('thumb');
const maskOffset = document.getElementById('maskOffset'); const go = $('go');
const maskBlur = document.getElementById('maskBlur'); const dl = $('dl');
const cropChk = document.getElementById('crop'); const dlbtn = $('dlbtn');
const cropMargin = document.getElementById('cropMargin'); 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 modelSel = $('model');
const ctlPrompt = document.getElementById('ctl-prompt'); 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 selectedFile = null;
let tab = 'auto'; let mode = 'auto';
const HINTS = { 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.', 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) { /* ---- mode ---- */
tab = name; function setGroupEnabled(group, enabled) {
document.querySelectorAll('.tab').forEach(t => group.classList.toggle('disabled', !enabled);
t.classList.toggle('active', t.dataset.tab === name)); group.querySelectorAll('input, select').forEach(el => { el.disabled = !enabled; });
ctlAuto.hidden = name !== 'auto';
ctlPrompt.hidden = name !== 'prompt';
go.textContent = name === 'auto' ? 'Remove background' : 'Segment';
hint.textContent = HINTS[name];
} }
document.querySelectorAll('.tab').forEach(t => function setMode(m) {
t.addEventListener('click', () => setTab(t.dataset.tab))); mode = m;
cropChk.addEventListener('change', () => { cropMargin.disabled = !cropChk.checked; }); 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) { function setStatus(msg, isErr) {
statusEl.textContent = msg; statusEl.textContent = msg;
statusEl.className = 'status' + (isErr ? ' err' : ''); 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) { function pickFile(file) {
if (!file || !file.type.startsWith('image/')) { if (!file || !file.type.startsWith('image/')) {
setStatus('Please choose an image file.', true); setStatus('PLEASE CHOOSE AN IMAGE FILE.', true);
return; return;
} }
selectedFile = file; selectedFile = file;
fname.textContent = file.name + ' (' + Math.round(file.size / 1024) + ' KB)'; const url = URL.createObjectURL(file);
srcImg.src = 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'); outImg.removeAttribute('src');
dlbtn.disabled = true; dlbtn.disabled = true;
go.disabled = false; go.disabled = false;
setStatus(''); setStatus('');
refreshTabs();
showView('original');
} }
drop.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', e => pickFile(e.target.files[0])); fileInput.addEventListener('change', e => pickFile(e.target.files[0]));
['dragenter', 'dragover'].forEach(ev => ['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 => ['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])); 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) { function fileToBase64(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const r = new FileReader(); 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.onerror = reject;
r.readAsDataURL(file); r.readAsDataURL(file);
}); });
} }
// --- lightbox: click to inspect, scroll to zoom, drag to pan --- /* ---- lightbox ---- */
const lightbox = document.getElementById('lightbox'); const lightbox = $('lightbox');
const lbStage = document.getElementById('lbStage'); const lbStage = $('lbStage');
const lbImg = document.getElementById('lbImg'); const lbImg = $('lbImg');
const lbClose = document.getElementById('lbClose'); const lbClose = $('lbClose');
let lbScale = 1, lbTx = 0, lbTy = 0, lbDrag = null; let lbScale = 1, lbTx = 0, lbTy = 0, lbDrag = null;
function lbApply() { function lbApply() { lbImg.style.transform = `translate(${lbTx}px, ${lbTy}px) scale(${lbScale})`; }
lbImg.style.transform = `translate(${lbTx}px, ${lbTy}px) scale(${lbScale})`;
}
function lbReset() { lbScale = 1; lbTx = 0; lbTy = 0; lbApply(); } function lbReset() { lbScale = 1; lbTx = 0; lbTy = 0; lbApply(); }
function openLightbox(src, isResult) { function openLightbox(src, isResult) {
@ -328,6 +366,11 @@ lightbox.addEventListener('mousedown', e => {
}); });
document.addEventListener('keydown', e => { document.addEventListener('keydown', e => {
if (e.key === 'Escape' && !lightbox.hidden) closeLightbox(); 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 => { lbStage.addEventListener('wheel', e => {
@ -361,15 +404,16 @@ window.addEventListener('mouseup', () => {
}); });
lbImg.addEventListener('dblclick', e => { e.preventDefault(); lbReset(); }); lbImg.addEventListener('dblclick', e => { e.preventDefault(); lbReset(); });
/* ---- run ---- */
go.addEventListener('click', async () => { go.addEventListener('click', async () => {
if (!selectedFile) return; if (!selectedFile) return;
if (tab === 'prompt' && !promptInput.value.trim()) { if (mode === 'prompt' && !promptInput.value.trim()) {
setStatus('Enter a prompt describing what to keep.', true); setStatus('ENTER A PROMPT DESCRIBING WHAT TO KEEP.', true);
return; return;
} }
go.disabled = true; go.disabled = true;
dlbtn.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(); const t0 = performance.now();
try { try {
const b64 = await fileToBase64(selectedFile); const b64 = await fileToBase64(selectedFile);
@ -382,7 +426,7 @@ go.addEventListener('click', async () => {
crop_margin: parseFloat(cropMargin.value) || 0, crop_margin: parseFloat(cropMargin.value) || 0,
}; };
let endpoint, body; let endpoint, body;
if (tab === 'auto') { if (mode === 'auto') {
endpoint = '/predict'; endpoint = '/predict';
body = { ...shared, model: modelSel.value, resolution: parseInt(resSel.value, 10) }; body = { ...shared, model: modelSel.value, resolution: parseInt(resSel.value, 10) };
} else { } else {
@ -403,14 +447,16 @@ go.addEventListener('click', async () => {
dl.href = dataUrl; dl.href = dataUrl;
dl.download = selectedFile.name.replace(/\.[^.]+$/, '') + '.png'; dl.download = selectedFile.name.replace(/\.[^.]+$/, '') + '.png';
dlbtn.disabled = false; dlbtn.disabled = false;
refreshTabs();
showView('result');
const secs = ((performance.now() - t0) / 1000).toFixed(1); const secs = ((performance.now() - t0) / 1000).toFixed(1);
if (tab === 'auto') { if (mode === 'auto') {
setStatus(`Done — ${data.width}×${data.height} · ${data.model} @ ${data.resolution} · ${secs}s`); setStatus(`DONE — ${data.width}×${data.height} · ${data.model} @ ${data.resolution} · ${secs}S`);
} else { } else {
const n = data.detections; const n = data.detections;
setStatus(`Done — ${n} object${n === 1 ? '' : 's'} matched "${data.prompt}" · ` + setStatus(`DONE — ${n} OBJECT${n === 1 ? '' : 'S'} MATCHED "${data.prompt}" · ` +
`${data.width}×${data.height} · ${secs}s` + `${data.width}×${data.height} · ${secs}S` +
(n === 0 ? ' (try a lower box threshold)' : '')); (n === 0 ? ' (TRY A LOWER BOX THRESHOLD)' : ''));
} }
} catch (err) { } catch (err) {
setStatus(err.message || String(err), true); setStatus(err.message || String(err), true);
@ -419,7 +465,10 @@ go.addEventListener('click', async () => {
} }
}); });
setTab('auto'); /* ---- init ---- */
setMode('auto');
syncCrop();
refreshTabs();
</script> </script>
</body> </body>
</html> </html>

View File

@ -652,5 +652,368 @@ label {
/* Rotate caret when drawer is open */ /* Rotate caret when drawer is open */
.drawer.open .drawer-caret { .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;
} }