Compare commits
No commits in common. "a544572e8ac61a6098fd4e68382f0a55ab79ee16" and "746635b570970925f8d8d81c4598a54150ee9f87" have entirely different histories.
a544572e8a
...
746635b570
28
README.md
28
README.md
@ -30,37 +30,15 @@ while a model downloads and loads.
|
|||||||
|
|
||||||
### Web UI
|
### Web UI
|
||||||
|
|
||||||
Open **http://localhost:8000/** — a single-page test app (handy over SSH):
|
Open **http://localhost:8000/** — a two-tab test page (handy over SSH):
|
||||||
|
|
||||||
- **Auto remove** — pick a model variant + resolution.
|
- **Auto remove** — pick a model variant + resolution.
|
||||||
- **Prompt segment** — type what to keep (e.g. `the dog`), tune the
|
- **Prompt segment** — type what to keep (e.g. `the dog`), tune the
|
||||||
GroundingDINO box / text thresholds.
|
GroundingDINO box / text thresholds.
|
||||||
|
|
||||||
Both modes support a transparency checkerboard preview, click-to-zoom lightbox,
|
Both tabs support a transparency checkerboard preview, click-to-zoom lightbox,
|
||||||
optional crop-to-subject, and download.
|
optional crop-to-subject, and download.
|
||||||
|
|
||||||
#### Keyboard shortcuts
|
|
||||||
|
|
||||||
The UI is fully keyboard-drivable. Shortcuts are ignored while typing in a
|
|
||||||
field and while Ctrl/Cmd/Alt is held.
|
|
||||||
|
|
||||||
| Key | Action |
|
|
||||||
|---------------------|-----------------------------------------------|
|
|
||||||
| `B` | Toggle the controls sidebar |
|
|
||||||
| `U` | Open the file picker to upload an image |
|
|
||||||
| `I` / `O` | Show the input / output image |
|
|
||||||
| `F` / `Z` | Open the zoom view for the visible image |
|
|
||||||
| `S` | Save (download PNG), once a result exists |
|
|
||||||
|
|
||||||
In the zoom view:
|
|
||||||
|
|
||||||
| Key | Action |
|
|
||||||
|---------------------------|-----------------------------------------|
|
|
||||||
| `F` / `Z` / `Esc` | Close the zoom view |
|
|
||||||
| `+` / `-` | Zoom in / out (1×–8×) |
|
|
||||||
| `0` | Reset zoom & pan |
|
|
||||||
| Arrows or `H` `J` `K` `L` | Pan (while zoomed past 1×) |
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### `POST /predict` — automatic background removal
|
### `POST /predict` — automatic background removal
|
||||||
@ -130,7 +108,7 @@ make dev # uv sync + run the server locally
|
|||||||
src/rmbg_as_a_service/model.py BiRefNet / RMBG-2.0 wrapper + compositing
|
src/rmbg_as_a_service/model.py BiRefNet / RMBG-2.0 wrapper + compositing
|
||||||
src/rmbg_as_a_service/prompt_segment.py GroundingDINO + SAM pipeline
|
src/rmbg_as_a_service/prompt_segment.py GroundingDINO + SAM pipeline
|
||||||
src/rmbg_as_a_service/server.py LitServe /predict + /segment + web UI
|
src/rmbg_as_a_service/server.py LitServe /predict + /segment + web UI
|
||||||
src/rmbg_as_a_service/static/ web UI (index.html + styles.css)
|
src/rmbg_as_a_service/static/ web UI (index.html)
|
||||||
scripts/client.py stdlib-only test client
|
scripts/client.py stdlib-only test client
|
||||||
Dockerfile / compose.yml CUDA image + nvidia runtime
|
Dockerfile / compose.yml CUDA image + nvidia runtime
|
||||||
Makefile build / run / test shortcuts
|
Makefile build / run / test shortcuts
|
||||||
|
|||||||
@ -1,32 +1,12 @@
|
|||||||
# BiRefNet background removal service — installs the published package on the CUDA 12.6 runtime.
|
FROM python:3.13-slim
|
||||||
# Same CUDA base family as the main ../Dockerfile, but installs rmbg-as-a-service from TestPyPI
|
|
||||||
# with a plain pip rather than building from the local source tree.
|
|
||||||
FROM nvidia/cuda:12.6.1-cudnn-runtime-ubuntu24.04
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PIP_BREAK_SYSTEM_PACKAGES=1 \
|
|
||||||
HF_HOME=/app/hf_cache \
|
HF_HOME=/app/hf_cache \
|
||||||
HF_HUB_ENABLE_HF_TRANSFER=1 \
|
HF_HUB_ENABLE_HF_TRANSFER=1 \
|
||||||
PORT=8000
|
PORT=8000
|
||||||
|
|
||||||
# Ubuntu 24.04 ships Python 3.12 (the project requires >=3.12).
|
RUN pip install rmbg-as-a-service hf-transfer
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends python3 python3-pip ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Fetch only the rmbg-as-a-service wheel from TestPyPI (--no-deps), so TestPyPI never takes
|
|
||||||
# part in dependency resolution -- otherwise junk squatted packages there (e.g. "FASTAPI")
|
|
||||||
# outrank the real ones. Then install it with real PyPI as the index, CUDA torch from the
|
|
||||||
# PyTorch index (its +cu126 build outranks the plain wheel by local-version ordering).
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
||||||
pip download --no-deps --dest /tmp/pkg \
|
|
||||||
--index-url https://test.pypi.org/simple/ \
|
|
||||||
"rmbg-as-a-service==0.0.2" \
|
|
||||||
&& pip install \
|
|
||||||
--extra-index-url https://download.pytorch.org/whl/cu126 \
|
|
||||||
/tmp/pkg/*.whl hf-transfer \
|
|
||||||
&& rm -rf /tmp/pkg
|
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
CMD ["rmbg-as-a-service"]
|
CMD ["rmbg-as-a-service"]
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
# mindthemath/rmbg image — build the published-package container, test it, push it.
|
|
||||||
# Override inline, e.g.: make release TAG=0.0.2
|
|
||||||
|
|
||||||
COMPOSE ?= docker compose
|
|
||||||
PYTHON ?= python3
|
|
||||||
IMAGE ?= mindthemath/rmbg
|
|
||||||
TAG ?= latest
|
|
||||||
PORT ?= 8000
|
|
||||||
INPUT ?= ../test.jpg
|
|
||||||
OUTPUT ?= output.png
|
|
||||||
BG ?= alpha
|
|
||||||
|
|
||||||
# Exported so compose.yml's ${TAG} interpolation picks it up.
|
|
||||||
export TAG
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
|
|
||||||
.PHONY: help build run up stop down logs log ps shell test push release clean
|
|
||||||
|
|
||||||
help: ## Show this help
|
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) \
|
|
||||||
| awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
|
|
||||||
|
|
||||||
build: ## Build the mindthemath/rmbg image
|
|
||||||
$(COMPOSE) build
|
|
||||||
|
|
||||||
run up: ## Start the service (GPU) in the background
|
|
||||||
$(COMPOSE) up -d
|
|
||||||
|
|
||||||
stop down: ## Stop and remove the service container
|
|
||||||
$(COMPOSE) down
|
|
||||||
|
|
||||||
logs log: ## Follow service logs
|
|
||||||
$(COMPOSE) logs -f
|
|
||||||
|
|
||||||
ps: ## Show service status
|
|
||||||
$(COMPOSE) ps
|
|
||||||
|
|
||||||
shell: ## Open a shell inside a fresh container
|
|
||||||
$(COMPOSE) run --rm --entrypoint bash rmbg
|
|
||||||
|
|
||||||
test: ## Send INPUT to the running service, save OUTPUT
|
|
||||||
$(PYTHON) ../scripts/client.py --url http://localhost:$(PORT) \
|
|
||||||
--input $(INPUT) --output $(OUTPUT) --background $(BG)
|
|
||||||
|
|
||||||
push: ## Push mindthemath/rmbg:$(TAG) to Docker Hub (needs docker login)
|
|
||||||
docker push $(IMAGE):$(TAG)
|
|
||||||
|
|
||||||
release: build push ## Build then push mindthemath/rmbg:$(TAG)
|
|
||||||
|
|
||||||
clean: ## Stop the service and remove the built image
|
|
||||||
-$(COMPOSE) down
|
|
||||||
-docker image rm $(IMAGE):$(TAG)
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
# mindthemath/rmbg — runs the image built from the published (TestPyPI) package.
|
|
||||||
services:
|
|
||||||
rmbg:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
image: mindthemath/rmbg:${TAG:-latest}
|
|
||||||
container_name: rmbg
|
|
||||||
ports:
|
|
||||||
- "${PORT:-8001}:8000"
|
|
||||||
environment:
|
|
||||||
- NVIDIA_VISIBLE_DEVICES=all
|
|
||||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
|
||||||
# Default variant/resolution; both are also selectable per request.
|
|
||||||
- BIREFNET_MODEL=${BIREFNET_MODEL:-general}
|
|
||||||
- BIREFNET_RESOLUTION=${BIREFNET_RESOLUTION:-1024}
|
|
||||||
# Use the nvidia-container-runtime for GPU acceleration.
|
|
||||||
runtime: nvidia
|
|
||||||
volumes:
|
|
||||||
# Persist downloaded BiRefNet weights across container restarts.
|
|
||||||
- hf-cache:/app/hf_cache
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
|
|
||||||
interval: 15s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
start_period: 180s
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
hf-cache:
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "rmbg-as-a-service"
|
name = "rmbg-as-a-service"
|
||||||
version = "0.0.3dev1"
|
version = "0.0.2"
|
||||||
description = "Background removal as a GPU-accelerated API"
|
description = "Background removal as a GPU-accelerated API"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@ -161,7 +161,7 @@
|
|||||||
<!-- lightbox -->
|
<!-- lightbox -->
|
||||||
<div id="lightbox" class="lightbox" hidden>
|
<div id="lightbox" class="lightbox" hidden>
|
||||||
<div class="lb-bar">
|
<div class="lb-bar">
|
||||||
<span>SCROLL / +− ZOOM · DRAG OR HJKL / ARROWS PAN · 0 RESETS · F / Z / ESC CLOSE</span>
|
<span>SCROLL TO ZOOM · DRAG TO PAN · DOUBLE-CLICK RESETS · ESC CLOSES</span>
|
||||||
<button class="lb-close" id="lbClose" title="Close">✕</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>
|
||||||
@ -350,39 +350,6 @@ function openLightbox(src, isResult) {
|
|||||||
}
|
}
|
||||||
function closeLightbox() { lightbox.hidden = true; lbImg.removeAttribute('src'); }
|
function closeLightbox() { lightbox.hidden = true; lbImg.removeAttribute('src'); }
|
||||||
|
|
||||||
/* which preview image is currently shown */
|
|
||||||
function currentImg() {
|
|
||||||
return tabOutput.classList.contains('active') ? outImg : srcImg;
|
|
||||||
}
|
|
||||||
/* 'f' / 'z' — open the zoom view for the visible image, or close it */
|
|
||||||
function toggleZoom() {
|
|
||||||
if (!lightbox.hidden) { closeLightbox(); return; }
|
|
||||||
const img = currentImg();
|
|
||||||
const src = img.getAttribute('src');
|
|
||||||
if (src) openLightbox(src, img === outImg);
|
|
||||||
}
|
|
||||||
/* keyboard zoom — anchored on the centre of the stage */
|
|
||||||
function lbZoom(factor) {
|
|
||||||
const stageRect = lbStage.getBoundingClientRect();
|
|
||||||
const imgRect = lbImg.getBoundingClientRect();
|
|
||||||
const cx = stageRect.left + stageRect.width / 2 - imgRect.left;
|
|
||||||
const cy = stageRect.top + stageRect.height / 2 - imgRect.top;
|
|
||||||
const newScale = Math.min(8, Math.max(1, lbScale * factor));
|
|
||||||
const ratio = newScale / lbScale;
|
|
||||||
lbTx -= cx * (ratio - 1);
|
|
||||||
lbTy -= cy * (ratio - 1);
|
|
||||||
lbScale = newScale;
|
|
||||||
if (lbScale === 1) { lbTx = 0; lbTy = 0; }
|
|
||||||
lbApply();
|
|
||||||
}
|
|
||||||
/* keyboard pan — dx/dy in {-1,0,1}; only meaningful while zoomed in */
|
|
||||||
function lbPan(dx, dy) {
|
|
||||||
if (lbScale <= 1) return;
|
|
||||||
lbTx += dx * 80;
|
|
||||||
lbTy += dy * 80;
|
|
||||||
lbApply();
|
|
||||||
}
|
|
||||||
|
|
||||||
srcImg.addEventListener('click', () => openLightbox(srcImg.getAttribute('src'), false));
|
srcImg.addEventListener('click', () => openLightbox(srcImg.getAttribute('src'), false));
|
||||||
outImg.addEventListener('click', () => openLightbox(outImg.getAttribute('src'), true));
|
outImg.addEventListener('click', () => openLightbox(outImg.getAttribute('src'), true));
|
||||||
lbClose.addEventListener('click', closeLightbox);
|
lbClose.addEventListener('click', closeLightbox);
|
||||||
@ -390,33 +357,14 @@ lightbox.addEventListener('mousedown', e => {
|
|||||||
if (e.target === lightbox || e.target === lbStage) closeLightbox();
|
if (e.target === lightbox || e.target === lbStage) closeLightbox();
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape' && !lightbox.hidden) closeLightbox();
|
||||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||||
const k = e.key.toLowerCase();
|
|
||||||
|
|
||||||
// --- zoom view: capture all navigation keys while it's open ---
|
|
||||||
if (!lightbox.hidden) {
|
|
||||||
if (e.key === 'Escape' || k === 'f' || k === 'z') closeLightbox();
|
|
||||||
else if (k === '+' || k === '=') lbZoom(1.25);
|
|
||||||
else if (k === '-' || k === '_') lbZoom(1 / 1.25);
|
|
||||||
else if (k === '0') lbReset();
|
|
||||||
else if (e.key === 'ArrowLeft' || k === 'h') lbPan( 1, 0);
|
|
||||||
else if (e.key === 'ArrowRight' || k === 'l') lbPan(-1, 0);
|
|
||||||
else if (e.key === 'ArrowUp' || k === 'k') lbPan( 0, 1);
|
|
||||||
else if (e.key === 'ArrowDown' || k === 'j') lbPan( 0, -1);
|
|
||||||
else return;
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- main UI shortcuts (ignored while typing in a field) ---
|
|
||||||
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();
|
||||||
if (k === 'b') toggleSidebar();
|
if (k === 'b') toggleSidebar();
|
||||||
else if (k === 'u') fileInput.click();
|
|
||||||
else if (k === 'i') showView('input');
|
else if (k === 'i') showView('input');
|
||||||
else if (k === 'o') showView('output');
|
else if (k === 'o') showView('output');
|
||||||
else if (k === 'f' || k === 'z') toggleZoom();
|
|
||||||
else if (k === 's' && !dlbtn.disabled) dl.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
lbStage.addEventListener('wheel', e => {
|
lbStage.addEventListener('wheel', e => {
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@ -1163,7 +1163,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rmbg-as-a-service"
|
name = "rmbg-as-a-service"
|
||||||
version = "0.0.3.dev1"
|
version = "0.0.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "einops" },
|
{ name = "einops" },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user