Compare commits

..

No commits in common. "22696f3ec7cae05f7a6e4c16c0a5daea90d8406b" and "27d9494123928d189a2a08eb37d4eeecf78a0599" have entirely different histories.

4 changed files with 259 additions and 684 deletions

View File

@ -29,13 +29,6 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/rmbg_as_a_service"] packages = ["src/rmbg_as_a_service"]
[tool.hatch.build.targets.sdist]
include = [
"src/rmbg_as_a_service",
"README.md",
"pyproject.toml",
]
# BiRefNet (torch) needs CUDA wheels; pull torch/torchvision from the PyTorch index. # BiRefNet (torch) needs CUDA wheels; pull torch/torchvision from the PyTorch index.
[[tool.uv.index]] [[tool.uv.index]]
name = "pytorch-cu126" name = "pytorch-cu126"

View File

@ -41,15 +41,13 @@ 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, Response from fastapi.responses import HTMLResponse
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
_STATIC = Path(__file__).parent / "static" _UI_HTML = (Path(__file__).parent / "static" / "index.html").read_text(encoding="utf-8")
_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
@ -134,10 +132,6 @@ 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,350 +3,312 @@
<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>RMBG_SERVICE // BACKGROUND REMOVAL</title> <title>Background Removal &amp; Segmentation</title>
<link rel="stylesheet" href="/styles.css" /> <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> </head>
<body> <body>
<div class="container" id="container"> <div class="wrap">
<h1>Background Removal &amp; 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">&#8249;</button> <div class="tabs">
<button class="tab active" data-tab="auto">Auto remove</button>
<!-- ============ CONTROL SIDEBAR ============ --> <button class="tab" data-tab="prompt">Prompt segment</button>
<aside class="controls panel">
<header class="app-header">
<div class="app-title">RMBG_SERVICE</div>
<div class="app-sub">[ BACKGROUND REMOVAL // SEGMENTATION ]</div>
</header>
<!-- 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>
<!-- 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>
<!-- AUTO controls --> <div id="drop">
<div class="control-group" id="ctl-auto"> <p><strong>Drop an image here</strong> or click to choose</p>
<div class="control-label">// MODEL</div> <p id="fname">No file selected</p>
<div class="select-wrap"> <input id="file" type="file" accept="image/*" hidden />
</div>
<!-- Auto (BiRefNet / RMBG-2.0) controls -->
<div class="controls" id="ctl-auto">
<label class="field">Model
<select id="model"> <select id="model">
<option value="general">GENERAL &mdash; CLEAN SINGLE SUBJECTS (FAST)</option> <option value="general">general — clean single subjects (fast)</option>
<option value="HR" selected>HR &mdash; LARGE / DETAILED SCENES</option> <option value="HR" selected>HR — large / detailed scenes</option>
<option value="portrait">PORTRAIT &mdash; PEOPLE</option> <option value="portrait">portrait — people</option>
<option value="matting">MATTING &mdash; SOFT EDGES / HAIR</option> <option value="matting">matting — soft edges / hair</option>
<option value="lite">LITE &mdash; FASTEST</option> <option value="lite">lite — fastest</option>
<option value="rmbg2">RMBG2 &mdash; BRIA RMBG-2.0</option> <option value="rmbg2">rmbg2 — BRIA RMBG-2.0</option>
</select> </select>
</div> </label>
<div class="control-label">// RESOLUTION</div> <label class="field">Resolution
<div class="select-wrap">
<select id="resolution"> <select id="resolution">
<option value="1024">1024</option> <option value="1024">1024</option>
<option value="1536">1536</option> <option value="1536">1536</option>
<option value="2048">2048</option> <option value="2048">2048</option>
<option value="2560" selected>2560</option> <option value="2560" selected>2560</option>
</select> </select>
</div> </label>
</div> </div>
<!-- PROMPT controls --> <!-- Prompt (GroundingDINO + SAM) controls -->
<div class="control-group" id="ctl-prompt"> <div class="controls" id="ctl-prompt" hidden>
<div class="control-label">// PROMPT &mdash; WHAT TO KEEP</div> <label class="field">Prompt — what to keep
<input type="text" id="prompt" placeholder="e.g. THE DOG . COW. PERSON." /> <input type="text" id="prompt" placeholder="e.g. the dog · cow. person." />
</label>
<div class="slider-container"> <label class="field">
<div class="control-label">BOX THRESHOLD <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>
<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> <input type="number" id="boxThr" value="0.3" min="0" max="1" step="0.05" />
</div> </label>
<input type="range" class="slider" id="boxThr" min="0" max="1" step="0.05" value="0.3" /> <label class="field">
<div class="slider-value" id="boxThrVal">0.30</div> <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> </div>
<div class="slider-container"> <!-- Shared output controls -->
<div class="control-label">TEXT THRESHOLD <div class="controls">
<span class="help" data-tip="How strongly a detection must match your prompt words. Lower = looser word matching; higher = stricter.">?</span> <label class="field">Background
</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"> <select id="background">
<option value="alpha" selected>TRANSPARENT</option> <option value="alpha" selected>transparent</option>
<option value="white">WHITE</option> <option value="white">white</option>
<option value="black">BLACK</option> <option value="black">black</option>
<option value="gray">GRAY</option> <option value="gray">gray</option>
<option value="green">GREEN</option> <option value="green">green</option>
<option value="blue">BLUE</option> <option value="blue">blue</option>
<option value="red">RED</option> <option value="red">red</option>
</select> </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>
<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> <div class="hint" id="hint"></div>
</aside> <div class="go-row">
<button class="go" id="go" disabled>Remove background</button>
<!-- ============ PREVIEW ============ --> <a id="dl" download="cutout.png"><button id="dlbtn" class="ghost" disabled>Download PNG</button></a>
<main class="preview-container panel"> <span id="status" class="status"></span>
<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>
<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 class="panels">
<div class="panel">
<h2>Original</h2>
<div class="imgbox"><img id="src" alt="" /></div>
</div>
<div class="panel">
<h2>Result</h2>
<div class="imgbox checker"><img id="out" alt="" /></div>
</div>
</div>
</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 &middot; DRAG TO PAN &middot; DOUBLE-CLICK RESETS &middot; ESC CLOSES</span> <span>scroll to zoom · drag to pan · double-click resets · Esc closes</span>
<button class="lb-close" id="lbClose" title="Close">&#10005;</button> <button class="lb-close" id="lbClose" title="Close"></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 $ = 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 modelSel = document.getElementById('model');
const sidebarToggle = $('sidebarToggle'); const resSel = document.getElementById('resolution');
const drop = $('drop'); const promptInput = document.getElementById('prompt');
const fileInput = $('file'); const boxThr = document.getElementById('boxThr');
const fname = $('fname'); const textThr = document.getElementById('textThr');
const thumb = $('thumb'); const bgSel = document.getElementById('background');
const go = $('go'); const maskOffset = document.getElementById('maskOffset');
const dl = $('dl'); const maskBlur = document.getElementById('maskBlur');
const dlbtn = $('dlbtn'); const cropChk = document.getElementById('crop');
const statusEl = $('status'); const cropMargin = document.getElementById('cropMargin');
const hint = $('hint');
const reset = $('reset');
const srcImg = $('src');
const outImg = $('out');
const noPreview = $('noPreview');
const modelSel = $('model'); const ctlAuto = document.getElementById('ctl-auto');
const resSel = $('resolution'); const ctlPrompt = document.getElementById('ctl-prompt');
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 mode = 'auto'; let tab = '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.',
}; };
/* ---- mode ---- */ function setTab(name) {
function setGroupEnabled(group, enabled) { tab = name;
group.classList.toggle('disabled', !enabled); document.querySelectorAll('.tab').forEach(t =>
group.querySelectorAll('input, select').forEach(el => { el.disabled = !enabled; }); 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) { document.querySelectorAll('.tab').forEach(t =>
mode = m; t.addEventListener('click', () => setTab(t.dataset.tab)));
setGroupEnabled(ctlAuto, m === 'auto'); cropChk.addEventListener('change', () => { cropMargin.disabled = !cropChk.checked; });
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;
const url = URL.createObjectURL(file); fname.textContent = file.name + ' (' + Math.round(file.size / 1024) + ' KB)';
const kb = Math.round(file.size / 1024); srcImg.src = URL.createObjectURL(file);
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('dragover'); })); drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.add('over'); }));
['dragleave', 'drop'].forEach(ev => ['dragleave', 'drop'].forEach(ev =>
drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('dragover'); })); drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('over'); }));
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]); r.onload = () => resolve(r.result.split(',')[1]); // strip data URL prefix
r.onerror = reject; r.onerror = reject;
r.readAsDataURL(file); r.readAsDataURL(file);
}); });
} }
/* ---- lightbox ---- */ // --- lightbox: click to inspect, scroll to zoom, drag to pan ---
const lightbox = $('lightbox'); const lightbox = document.getElementById('lightbox');
const lbStage = $('lbStage'); const lbStage = document.getElementById('lbStage');
const lbImg = $('lbImg'); const lbImg = document.getElementById('lbImg');
const lbClose = $('lbClose'); const lbClose = document.getElementById('lbClose');
let lbScale = 1, lbTx = 0, lbTy = 0, lbDrag = null; 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 lbReset() { lbScale = 1; lbTx = 0; lbTy = 0; lbApply(); }
function openLightbox(src, isResult) { function openLightbox(src, isResult) {
@ -366,11 +328,6 @@ 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 => {
@ -404,16 +361,15 @@ 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 (mode === 'prompt' && !promptInput.value.trim()) { if (tab === '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);
@ -426,7 +382,7 @@ go.addEventListener('click', async () => {
crop_margin: parseFloat(cropMargin.value) || 0, crop_margin: parseFloat(cropMargin.value) || 0,
}; };
let endpoint, body; let endpoint, body;
if (mode === 'auto') { if (tab === '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 {
@ -447,16 +403,14 @@ 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 (mode === 'auto') { if (tab === '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);
@ -465,10 +419,7 @@ go.addEventListener('click', async () => {
} }
}); });
/* ---- init ---- */ setTab('auto');
setMode('auto');
syncCrop();
refreshTabs();
</script> </script>
</body> </body>
</html> </html>

View File

@ -652,368 +652,5 @@ label {
/* Rotate caret when drawer is open */ /* Rotate caret when drawer is open */
.drawer.open .drawer-caret { .drawer.open .drawer-caret {
transform: rotate(90deg); transform: rotate(120deg);
}
/* ============================================================
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;
} }