Compare commits
No commits in common. "3f24ce2f800340375334510fe7daa803cf946bf1" and "22696f3ec7cae05f7a6e4c16c0a5daea90d8406b" have entirely different histories.
3f24ce2f80
...
22696f3ec7
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,4 +6,3 @@ output.png
|
|||||||
output*.png
|
output*.png
|
||||||
mask*.png
|
mask*.png
|
||||||
*.jpg
|
*.jpg
|
||||||
.env
|
|
||||||
|
|||||||
8
Makefile
8
Makefile
@ -11,7 +11,7 @@ BLUR ?= 0
|
|||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
.PHONY: help build run up stop down logs log ps test test-mask dev sync shell clean fmt testpub
|
.PHONY: help build run up stop down logs log ps test test-mask dev sync shell clean fmt
|
||||||
|
|
||||||
help: ## Show this help
|
help: ## Show this help
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) \
|
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) \
|
||||||
@ -56,9 +56,3 @@ fmt: ## Format code with ruff
|
|||||||
clean: ## Stop the service and remove build artifacts
|
clean: ## Stop the service and remove build artifacts
|
||||||
-$(COMPOSE) down
|
-$(COMPOSE) down
|
||||||
rm -f $(OUTPUT) mask.png
|
rm -f $(OUTPUT) mask.png
|
||||||
|
|
||||||
testpub: ## Publish dist/ to TestPyPI (UV_PUBLISH_TOKEN from .env)
|
|
||||||
uv build
|
|
||||||
set -a && . ./.env && set +a && \
|
|
||||||
uv publish --publish-url https://test.pypi.org/legacy/ -t "$$UV_PUBLISH_TOKEN" --dry-run
|
|
||||||
|
|
||||||
|
|||||||
@ -125,8 +125,8 @@
|
|||||||
|
|
||||||
<div class="slider-container" id="cropMarginWrap">
|
<div class="slider-container" id="cropMarginWrap">
|
||||||
<div class="control-label">CROP MARGIN <span class="unit">IN</span></div>
|
<div class="control-label">CROP MARGIN <span class="unit">IN</span></div>
|
||||||
<input type="range" class="slider" id="cropMargin" min="0" max="2" step="0.25" value="0" />
|
<input type="range" class="slider" id="cropMargin" min="0" max="5" step="0.1" value="0" />
|
||||||
<div class="slider-value" id="cropMarginVal">0.00</div>
|
<div class="slider-value" id="cropMarginVal">0.0 IN</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -140,14 +140,15 @@
|
|||||||
<button class="reset-btn" id="reset">✕ CLEAR</button>
|
<button class="reset-btn" id="reset">✕ CLEAR</button>
|
||||||
|
|
||||||
<div class="status" id="status"></div>
|
<div class="status" id="status"></div>
|
||||||
|
<div class="hint" id="hint"></div>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- ============ PREVIEW ============ -->
|
<!-- ============ PREVIEW ============ -->
|
||||||
<main class="preview-container panel">
|
<main class="preview-container panel">
|
||||||
<div class="preview-tabs">
|
<div class="preview-tabs">
|
||||||
<button class="preview-tab active" id="tabInput" data-view="input" disabled>INPUT [I]</button>
|
<button class="preview-tab active" id="tabOriginal" data-view="original" disabled>ORIGINAL</button>
|
||||||
<button class="preview-tab" id="tabOutput" data-view="output" disabled>OUTPUT [O]</button>
|
<button class="preview-tab" id="tabResult" data-view="result" disabled>RESULT</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-stage">
|
<div class="preview-stage">
|
||||||
<div class="no-preview" id="noPreview">[ NO IMAGE LOADED ]</div>
|
<div class="no-preview" id="noPreview">[ NO IMAGE LOADED ]</div>
|
||||||
@ -180,6 +181,7 @@ const go = $('go');
|
|||||||
const dl = $('dl');
|
const dl = $('dl');
|
||||||
const dlbtn = $('dlbtn');
|
const dlbtn = $('dlbtn');
|
||||||
const statusEl = $('status');
|
const statusEl = $('status');
|
||||||
|
const hint = $('hint');
|
||||||
const reset = $('reset');
|
const reset = $('reset');
|
||||||
const srcImg = $('src');
|
const srcImg = $('src');
|
||||||
const outImg = $('out');
|
const outImg = $('out');
|
||||||
@ -199,12 +201,17 @@ const cropMarginWrap = $('cropMarginWrap');
|
|||||||
|
|
||||||
const ctlAuto = $('ctl-auto');
|
const ctlAuto = $('ctl-auto');
|
||||||
const ctlPrompt = $('ctl-prompt');
|
const ctlPrompt = $('ctl-prompt');
|
||||||
const tabInput = $('tabInput');
|
const tabOriginal= $('tabOriginal');
|
||||||
const tabOutput = $('tabOutput');
|
const tabResult = $('tabResult');
|
||||||
|
|
||||||
let selectedFile = null;
|
let selectedFile = null;
|
||||||
let mode = '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.',
|
||||||
|
prompt: 'Type what to keep, e.g. "the dog" (or several: "cow. person."). Lower the box threshold to detect more / fainter objects.',
|
||||||
|
};
|
||||||
|
|
||||||
/* ---- mode ---- */
|
/* ---- mode ---- */
|
||||||
function setGroupEnabled(group, enabled) {
|
function setGroupEnabled(group, enabled) {
|
||||||
group.classList.toggle('disabled', !enabled);
|
group.classList.toggle('disabled', !enabled);
|
||||||
@ -215,6 +222,7 @@ function setMode(m) {
|
|||||||
setGroupEnabled(ctlAuto, m === 'auto');
|
setGroupEnabled(ctlAuto, m === 'auto');
|
||||||
setGroupEnabled(ctlPrompt, m === 'prompt');
|
setGroupEnabled(ctlPrompt, m === 'prompt');
|
||||||
go.textContent = m === 'auto' ? 'REMOVE BACKGROUND' : 'SEGMENT';
|
go.textContent = m === 'auto' ? 'REMOVE BACKGROUND' : 'SEGMENT';
|
||||||
|
hint.textContent = HINTS[m];
|
||||||
}
|
}
|
||||||
document.querySelectorAll('input[name="mode"]').forEach(r =>
|
document.querySelectorAll('input[name="mode"]').forEach(r =>
|
||||||
r.addEventListener('change', () => setMode(r.value)));
|
r.addEventListener('change', () => setMode(r.value)));
|
||||||
@ -240,7 +248,7 @@ wireSlider(boxThr, $('boxThrVal'), v => (+v).toFixed(2));
|
|||||||
wireSlider(textThr, $('textThrVal'), v => (+v).toFixed(2));
|
wireSlider(textThr, $('textThrVal'), v => (+v).toFixed(2));
|
||||||
wireSlider(maskOffset, $('maskOffsetVal'), v => (v > 0 ? '+' : '') + v);
|
wireSlider(maskOffset, $('maskOffsetVal'), v => (v > 0 ? '+' : '') + v);
|
||||||
wireSlider(maskBlur, $('maskBlurVal'), v => String(v));
|
wireSlider(maskBlur, $('maskBlurVal'), v => String(v));
|
||||||
wireSlider(cropMargin, $('cropMarginVal'), v => (+v).toFixed(2));
|
wireSlider(cropMargin, $('cropMarginVal'), v => (+v).toFixed(1) + ' IN');
|
||||||
|
|
||||||
function syncCrop() {
|
function syncCrop() {
|
||||||
cropMarginWrap.classList.toggle('disabled', !cropChk.checked);
|
cropMarginWrap.classList.toggle('disabled', !cropChk.checked);
|
||||||
@ -257,17 +265,17 @@ function setStatus(msg, isErr) {
|
|||||||
function refreshTabs() {
|
function refreshTabs() {
|
||||||
const hasSrc = !!srcImg.getAttribute('src');
|
const hasSrc = !!srcImg.getAttribute('src');
|
||||||
const hasOut = !!outImg.getAttribute('src');
|
const hasOut = !!outImg.getAttribute('src');
|
||||||
tabInput.disabled = !hasSrc;
|
tabOriginal.disabled = !hasSrc;
|
||||||
tabOutput.disabled = !hasOut;
|
tabResult.disabled = !hasOut;
|
||||||
noPreview.hidden = hasSrc || hasOut;
|
noPreview.hidden = hasSrc || hasOut;
|
||||||
}
|
}
|
||||||
function showView(v) {
|
function showView(v) {
|
||||||
if ((v === 'input' && tabInput.disabled) ||
|
if ((v === 'original' && tabOriginal.disabled) ||
|
||||||
(v === 'output' && tabOutput.disabled)) return;
|
(v === 'result' && tabResult.disabled)) return;
|
||||||
tabInput.classList.toggle('active', v === 'input');
|
tabOriginal.classList.toggle('active', v === 'original');
|
||||||
tabOutput.classList.toggle('active', v === 'output');
|
tabResult.classList.toggle('active', v === 'result');
|
||||||
srcImg.hidden = v !== 'input';
|
srcImg.hidden = v !== 'original';
|
||||||
outImg.hidden = v !== 'output';
|
outImg.hidden = v !== 'result';
|
||||||
}
|
}
|
||||||
document.querySelectorAll('.preview-tab').forEach(t =>
|
document.querySelectorAll('.preview-tab').forEach(t =>
|
||||||
t.addEventListener('click', () => showView(t.dataset.view)));
|
t.addEventListener('click', () => showView(t.dataset.view)));
|
||||||
@ -295,7 +303,7 @@ function pickFile(file) {
|
|||||||
go.disabled = false;
|
go.disabled = false;
|
||||||
setStatus('');
|
setStatus('');
|
||||||
refreshTabs();
|
refreshTabs();
|
||||||
showView('input');
|
showView('original');
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInput.addEventListener('change', e => pickFile(e.target.files[0]));
|
fileInput.addEventListener('change', e => pickFile(e.target.files[0]));
|
||||||
@ -318,8 +326,8 @@ reset.addEventListener('click', () => {
|
|||||||
dlbtn.disabled = true;
|
dlbtn.disabled = true;
|
||||||
setStatus('');
|
setStatus('');
|
||||||
refreshTabs();
|
refreshTabs();
|
||||||
tabInput.classList.add('active');
|
tabOriginal.classList.add('active');
|
||||||
tabOutput.classList.remove('active');
|
tabResult.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
function fileToBase64(file) {
|
function fileToBase64(file) {
|
||||||
@ -358,13 +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.metaKey || e.ctrlKey || e.altKey) return;
|
if ((e.key === 'b' || e.key === 'B') && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||||
const t = e.target;
|
const t = e.target;
|
||||||
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return;
|
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return;
|
||||||
const k = e.key.toLowerCase();
|
toggleSidebar();
|
||||||
if (k === 'b') toggleSidebar();
|
}
|
||||||
else if (k === 'i') showView('input');
|
|
||||||
else if (k === 'o') showView('output');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
lbStage.addEventListener('wheel', e => {
|
lbStage.addEventListener('wheel', e => {
|
||||||
@ -442,7 +448,7 @@ go.addEventListener('click', async () => {
|
|||||||
dl.download = selectedFile.name.replace(/\.[^.]+$/, '') + '.png';
|
dl.download = selectedFile.name.replace(/\.[^.]+$/, '') + '.png';
|
||||||
dlbtn.disabled = false;
|
dlbtn.disabled = false;
|
||||||
refreshTabs();
|
refreshTabs();
|
||||||
showView('output');
|
showView('result');
|
||||||
const secs = ((performance.now() - t0) / 1000).toFixed(1);
|
const secs = ((performance.now() - t0) / 1000).toFixed(1);
|
||||||
if (mode === '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`);
|
||||||
|
|||||||
@ -768,11 +768,11 @@ select option {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* stack OUTPUT-drawer controls with the same 5px rhythm as .control-group */
|
/* breathing room between stacked controls inside the OUTPUT drawer */
|
||||||
.drawer-content {
|
.drawer-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- help tooltip (carried over, restyled brutalist) --- */
|
/* --- help tooltip (carried over, restyled brutalist) --- */
|
||||||
@ -800,7 +800,7 @@ select option {
|
|||||||
.help:hover::after {
|
.help:hover::after {
|
||||||
content: attr(data-tip);
|
content: attr(data-tip);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 150%;
|
bottom: 150%;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
background: #000000;
|
background: #000000;
|
||||||
@ -866,6 +866,15 @@ select option {
|
|||||||
color: #ff6b6b;
|
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 area --- */
|
||||||
.preview-tabs {
|
.preview-tabs {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user