refactor styling: terminal
This commit is contained in:
parent
27d9494123
commit
15b23c37c1
@ -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)."""
|
||||
|
||||
@ -3,312 +3,350 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Background Removal & Segmentation</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
background: #15171c; color: #e8e8ea; padding: 24px;
|
||||
}
|
||||
h1 { font-size: 1.25rem; font-weight: 600; margin: 0 0 4px; }
|
||||
.sub { color: #8a8f99; font-size: .85rem; margin-bottom: 16px; }
|
||||
.wrap { max-width: 1100px; margin: 0 auto; }
|
||||
|
||||
.tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid #2a2f3a; }
|
||||
.tab { background: none; border: 0; color: #8a8f99; font-size: .9rem; font-weight: 600;
|
||||
padding: 10px 16px; cursor: pointer; border-bottom: 2px solid transparent; }
|
||||
.tab.active { color: #e8e8ea; border-bottom-color: #5b8cff; }
|
||||
|
||||
#drop {
|
||||
border: 2px dashed #3a3f4b; border-radius: 12px; padding: 36px;
|
||||
text-align: center; cursor: pointer; transition: border-color .15s, background .15s;
|
||||
}
|
||||
#drop.over { border-color: #5b8cff; background: #1c2230; }
|
||||
#drop p { margin: 6px 0; color: #8a8f99; }
|
||||
|
||||
.controls { display: flex; gap: 12px; align-items: flex-end; margin: 14px 0; flex-wrap: wrap; }
|
||||
.controls[hidden] { display: none; }
|
||||
label.field { display: flex; flex-direction: column; gap: 4px; font-size: .72rem;
|
||||
color: #8a8f99; text-transform: uppercase; letter-spacing: .04em; }
|
||||
select, input[type=number], input[type=text] {
|
||||
background: #2a2f3a; color: #e8e8ea; border: 1px solid #3a3f4b;
|
||||
border-radius: 8px; padding: 8px 10px; font-size: .9rem;
|
||||
}
|
||||
input[type=number] { width: 78px; }
|
||||
input[type=number]:disabled { opacity: .45; }
|
||||
input[type=text]#prompt { width: 320px; }
|
||||
.check { display: flex; align-items: center; gap: 6px; font-size: .85rem;
|
||||
color: #e8e8ea; cursor: pointer; align-self: end; padding-bottom: 8px; }
|
||||
.check input { width: 15px; height: 15px; accent-color: #5b8cff; cursor: pointer; }
|
||||
|
||||
/* help tooltips */
|
||||
.help { display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 14px; height: 14px; margin-left: 5px; border-radius: 50%;
|
||||
border: 1px solid #4a4f5b; color: #8a8f99; font-size: 9px; font-weight: 700;
|
||||
font-style: normal; cursor: help; position: relative; vertical-align: middle; }
|
||||
.help:hover { color: #e8e8ea; border-color: #5b8cff; }
|
||||
.help:hover::after {
|
||||
content: attr(data-tip); position: absolute; bottom: 150%; left: 50%;
|
||||
transform: translateX(-50%); width: 220px; background: #0c0d11;
|
||||
color: #d8d9dc; border: 1px solid #3a3f4b; border-radius: 6px;
|
||||
padding: 7px 9px; font-size: .72rem; font-weight: 400; line-height: 1.4;
|
||||
text-transform: none; letter-spacing: normal; white-space: normal;
|
||||
z-index: 50; pointer-events: none; }
|
||||
|
||||
button.go {
|
||||
background: #5b8cff; color: #fff; border: 0; border-radius: 8px;
|
||||
padding: 10px 18px; font-size: .9rem; cursor: pointer; font-weight: 600;
|
||||
}
|
||||
button.go:disabled { background: #3a3f4b; cursor: not-allowed; }
|
||||
button.ghost { background: #2a2f3a; color: #fff; border: 0; border-radius: 8px;
|
||||
padding: 10px 18px; font-size: .9rem; cursor: pointer; font-weight: 600; }
|
||||
.go-row { display: flex; gap: 12px; align-items: center; margin: 14px 0; flex-wrap: wrap; }
|
||||
.status { color: #8a8f99; font-size: .85rem; }
|
||||
.status.err { color: #ff6b6b; }
|
||||
.hint { color: #6b7280; font-size: .78rem; margin: -4px 0 4px; }
|
||||
|
||||
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
|
||||
.panel { background: #1c1f27; border-radius: 12px; padding: 12px; }
|
||||
.panel h2 { font-size: .8rem; font-weight: 600; color: #8a8f99; margin: 0 0 8px;
|
||||
text-transform: uppercase; letter-spacing: .05em; }
|
||||
.imgbox {
|
||||
min-height: 260px; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 8px; overflow: hidden;
|
||||
}
|
||||
.checker {
|
||||
background-image:
|
||||
linear-gradient(45deg, #2a2f3a 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #2a2f3a 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #2a2f3a 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #2a2f3a 75%);
|
||||
background-size: 22px 22px;
|
||||
background-position: 0 0, 0 11px, 11px -11px, -11px 0;
|
||||
background-color: #20242d;
|
||||
}
|
||||
.imgbox img { max-width: 100%; max-height: 70vh; display: block; }
|
||||
.imgbox img[src] { cursor: zoom-in; }
|
||||
@media (max-width: 720px) { .panels { grid-template-columns: 1fr; } }
|
||||
|
||||
/* lightbox */
|
||||
.lightbox { position: fixed; inset: 0; z-index: 100; background: rgba(12,13,17,.97);
|
||||
display: flex; align-items: center; justify-content: center; }
|
||||
.lightbox[hidden] { display: none; }
|
||||
.lb-stage { width: 100vw; height: 100vh; overflow: hidden;
|
||||
display: flex; align-items: center; justify-content: center; }
|
||||
.lb-stage img { max-width: 100vw; max-height: 100vh; transform-origin: 0 0;
|
||||
cursor: grab; user-select: none; -webkit-user-drag: none; will-change: transform; }
|
||||
.lb-stage.grabbing img { cursor: grabbing; }
|
||||
.lb-bar { position: fixed; top: 0; left: 0; right: 0; padding: 14px 20px;
|
||||
z-index: 2; display: flex; justify-content: space-between; align-items: center;
|
||||
color: #8a8f99; font-size: .8rem; pointer-events: none; }
|
||||
.lb-close { pointer-events: auto; background: #2a2f3a; color: #e8e8ea;
|
||||
border: 1px solid #3a3f4b; border-radius: 8px; width: 34px; height: 34px;
|
||||
font-size: 1rem; line-height: 1; padding: 0; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
<title>RMBG_SERVICE // BACKGROUND REMOVAL</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>Background Removal & 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">‹</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">▲ 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 — CLEAN SINGLE SUBJECTS (FAST)</option>
|
||||
<option value="HR" selected>HR — LARGE / DETAILED SCENES</option>
|
||||
<option value="portrait">PORTRAIT — PEOPLE</option>
|
||||
<option value="matting">MATTING — SOFT EDGES / HAIR</option>
|
||||
<option value="lite">LITE — FASTEST</option>
|
||||
<option value="rmbg2">RMBG2 — BRIA RMBG-2.0</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-label">// RESOLUTION</div>
|
||||
<div class="select-wrap">
|
||||
<select id="resolution">
|
||||
<option value="1024">1024</option>
|
||||
<option value="1536">1536</option>
|
||||
<option value="2048">2048</option>
|
||||
<option value="2560" selected>2560</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PROMPT controls -->
|
||||
<div class="control-group" id="ctl-prompt">
|
||||
<div class="control-label">// PROMPT — WHAT TO KEEP</div>
|
||||
<input type="text" id="prompt" placeholder="e.g. THE DOG . COW. PERSON." />
|
||||
|
||||
<div class="slider-container">
|
||||
<div class="control-label">BOX THRESHOLD
|
||||
<span class="help" data-tip="Minimum confidence for GroundingDINO to keep a detected box. Lower finds more (and looser) objects; higher keeps only strong matches.">?</span>
|
||||
</div>
|
||||
<input type="range" class="slider" id="boxThr" min="0" max="1" step="0.05" value="0.3" />
|
||||
<div class="slider-value" id="boxThrVal">0.30</div>
|
||||
</div>
|
||||
|
||||
<div class="slider-container">
|
||||
<div class="control-label">TEXT THRESHOLD
|
||||
<span class="help" data-tip="How strongly a detection must match your prompt words. Lower = looser word matching; higher = stricter.">?</span>
|
||||
</div>
|
||||
<input type="range" class="slider" id="textThr" min="0" max="1" step="0.05" value="0.25" />
|
||||
<div class="slider-value" id="textThrVal">0.25</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OUTPUT drawer -->
|
||||
<div class="control-group drawer" id="outputDrawer">
|
||||
<div class="drawer-header">
|
||||
<span class="drawer-caret">▸</span>
|
||||
<span class="section-title">OUTPUT</span>
|
||||
</div>
|
||||
<div class="drawer-content">
|
||||
|
||||
<div class="control-label">// BACKGROUND</div>
|
||||
<div class="select-wrap">
|
||||
<select id="background">
|
||||
<option value="alpha" selected>TRANSPARENT</option>
|
||||
<option value="white">WHITE</option>
|
||||
<option value="black">BLACK</option>
|
||||
<option value="gray">GRAY</option>
|
||||
<option value="green">GREEN</option>
|
||||
<option value="blue">BLUE</option>
|
||||
<option value="red">RED</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="slider-container">
|
||||
<div class="control-label">EDGE OFFSET <span class="unit">PX</span>
|
||||
<span class="help" data-tip="Grow (+) or shrink (-) the cutout edge by N pixels. A small negative value trims a leftover background-colored fringe around hair or fur.">?</span>
|
||||
</div>
|
||||
<input type="range" class="slider" id="maskOffset" min="-20" max="20" step="1" value="0" />
|
||||
<div class="slider-value" id="maskOffsetVal">0</div>
|
||||
</div>
|
||||
|
||||
<div class="slider-container">
|
||||
<div class="control-label">FEATHER <span class="unit">PX</span>
|
||||
<span class="help" data-tip="Gaussian blur applied to the mask edge, in pixels. Softens the cutout for smoother compositing onto a new background.">?</span>
|
||||
</div>
|
||||
<input type="range" class="slider" id="maskBlur" min="0" max="64" step="1" value="0" />
|
||||
<div class="slider-value" id="maskBlurVal">0</div>
|
||||
</div>
|
||||
|
||||
<label class="check-row"><input type="checkbox" id="crop" checked /> CROP TO SUBJECT</label>
|
||||
|
||||
<div class="slider-container" id="cropMarginWrap">
|
||||
<div class="control-label">CROP MARGIN <span class="unit">IN</span></div>
|
||||
<input type="range" class="slider" id="cropMargin" min="0" max="5" step="0.1" value="0" />
|
||||
<div class="slider-value" id="cropMarginVal">0.0 IN</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="action-row">
|
||||
<button class="action-btn primary" id="go" disabled>REMOVE BACKGROUND</button>
|
||||
<a id="dl" download="cutout.png"><button class="action-btn" id="dlbtn" disabled>▼ DOWNLOAD PNG</button></a>
|
||||
</div>
|
||||
<button class="reset-btn" id="reset">✕ CLEAR</button>
|
||||
|
||||
<div class="status" id="status"></div>
|
||||
<div class="hint" id="hint"></div>
|
||||
|
||||
</aside>
|
||||
|
||||
<!-- ============ PREVIEW ============ -->
|
||||
<main class="preview-container panel">
|
||||
<div class="preview-tabs">
|
||||
<button class="preview-tab active" id="tabOriginal" data-view="original" disabled>ORIGINAL</button>
|
||||
<button class="preview-tab" id="tabResult" data-view="result" disabled>RESULT</button>
|
||||
</div>
|
||||
<div class="preview-stage">
|
||||
<div class="no-preview" id="noPreview">[ NO IMAGE LOADED ]</div>
|
||||
<img class="preview-canvas" id="src" alt="" hidden />
|
||||
<img class="preview-canvas checker" id="out" alt="" hidden />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- lightbox -->
|
||||
<div id="lightbox" class="lightbox" hidden>
|
||||
<div class="lb-bar">
|
||||
<span>scroll to zoom · drag to pan · double-click resets · Esc closes</span>
|
||||
<button class="lb-close" id="lbClose" title="Close">✕</button>
|
||||
<span>SCROLL TO ZOOM · DRAG TO PAN · DOUBLE-CLICK RESETS · ESC CLOSES</span>
|
||||
<button class="lb-close" id="lbClose" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="lb-stage" id="lbStage"><img id="lbImg" alt="" /></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const 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 ? '›' : '‹';
|
||||
}
|
||||
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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user