Borg93/ranymizer

Python

0

48 commits

updated Jun 11, 2026

See the code

README


title: Ranymizer emoji: πŸ”’ colorFrom: indigo colorTo: purple sdk: gradio sdk_version: 6.14.0 app_file: backend/server.py pinned: true license: gpl-3.0

Ranymizer β€” Swedish screenshot redactor

OCR β†’ PII detection β†’ canvas editor for redacting Swedish screenshots before sharing. Two build targets from one codebase:

  • Showcase (Python gr.Server backend, ZeroGPU on HF Space): PaddleOCR 3.5 β†’ GLiNER2-PII β†’ JSON.
  • Desktop (Tauri 2.0, fully local): transformers.js β†’ ONNX Runtime Web β†’ WebGPU/WASM, no backend.

Stack: SvelteKit 2 Β· Svelte 5 (runes) Β· Tailwind 4 Β· shadcn-svelte Β· Bun Β· Tauri 2 Β· Biome.

.
β”œβ”€β”€ README.md
β”œβ”€β”€ Dockerfile                # showcase: Python + static SPA, EXPOSE 7860
β”œβ”€β”€ Makefile                  # `make install`, `make showcase-backend`, `make tauri-dev`, …
β”œβ”€β”€ example-images/           # (you create) drop test screenshots here for the showcase landing page
β”œβ”€β”€ backend/                  # Python β€” showcase target only
β”‚   β”œβ”€β”€ pyproject.toml        # uv project (name = "ranymizer")
β”‚   β”œβ”€β”€ .python-version       # 3.11
β”‚   β”œβ”€β”€ requirements.txt      # fallback for hosts that need pip
β”‚   β”œβ”€β”€ app.py                # PaddleOCR + GLiNER2 pipeline
β”‚   β”œβ”€β”€ server.py             # gr.Server: API routes + SvelteKit static mount
β”‚   └── README.md             # uv setup + run instructions
└── frontend/                 # SvelteKit 2 SPA β€” both targets
    β”œβ”€β”€ package.json          # bun-managed
    β”œβ”€β”€ biome.json            # formatter + linter (.ts / .json only β€” Svelte handled by svelte-check)
    β”œβ”€β”€ svelte.config.js      # adapter-static (right for BOTH targets)
    β”œβ”€β”€ vite.config.ts        # engine selected by VITE_ENGINE at build time
    β”œβ”€β”€ src-tauri/            # Tauri 2.0 shell (locked-down CSP, core:default capability only)
    └── src/
        β”œβ”€β”€ app.html / app.css
        β”œβ”€β”€ routes/           # +layout.ts has ssr=false + prerender=true (SPA)
        └── lib/
            β”œβ”€β”€ state.svelte.ts   # EditorState (depends on engine, not on Gradio)
            β”œβ”€β”€ api.ts            # @gradio/client wrapper (showcase only)
            β”œβ”€β”€ utils.ts          # cn() + shadcn-svelte type helpers
            β”œβ”€β”€ types.ts
            β”œβ”€β”€ engine/           # ← the seam between UI and inference
            β”‚   β”œβ”€β”€ types.ts      #   AnonymizerEngine interface
            β”‚   β”œβ”€β”€ index.ts      #   picks engine from import.meta.env.VITE_ENGINE
            β”‚   β”œβ”€β”€ gradio.ts     #   showcase: wraps @gradio/client
            β”‚   β”œβ”€β”€ local.ts      #   desktop: drives the worker
            β”‚   β”œβ”€β”€ worker.ts     #   transformers.js OCR + PII (ES module worker)
            β”‚   β”œβ”€β”€ webgpu.ts     #   auto β†’ webgpu β†’ wasm backend detection
            β”‚   └── models.ts     #   model registry + offline category meta
            └── components/
                β”œβ”€β”€ ui/       # shadcn-svelte primitives (button, card, toggle-group, badge, separator, toggle)
                β”œβ”€β”€ Landing.svelte / Editor.svelte / Canvas.svelte / Sidebar.svelte / Loading.svelte

Setup (one-time)

make install     # installs bun + rust + uv if missing, then frontend deps + backend venv

Manual install:

# bun (frontend), rust (Tauri), uv (backend Python)
curl -fsSL https://bun.sh/install | bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
curl -LsSf https://astral.sh/uv/install.sh | sh

cd frontend && bun install
cd backend  && uv sync

Run

The Makefile has every target β€” make (with no args) lists them. The common ones:

# ── Showcase (Python backend + SPA over network) ──────────────
make showcase-backend      # Python gr.Server on :7860
make showcase-frontend     # SvelteKit dev on :5173 (Gradio engine)

# ── Desktop (Tauri 2.0, fully local, no Python) ──────────────
make tauri-dev             # opens a native window, hot-reload
make tauri-build           # release bundle (.dmg/.app/.deb/.msi)

# ── Local engine in a browser (no Python, no native shell) ───
make local-dev             # http://localhost:5173 with VITE_ENGINE=local
make local-build           # static build with VITE_ENGINE=local

# ── Showcase in one Docker image ─────────────────────────────
make docker-build && make docker-run    # http://localhost:7860

CORS is open on the Python side for :5173 so the dev frontend can hit the backend on :7860. Edits to .svelte / .ts hot-reload. Edits to app.py or server.py require a Python restart.

Data flow

Same pattern as the BiRefNet demo: FileData in, JSON out. The source image is never round-tripped through the server β€” the SvelteKit client already has the File, so state.upload(file) calls URL.createObjectURL(file) and feeds it straight to <img>. The Python side returns only {filename, width, height, boxes, text, spans}. /api/meta is a separate plain GET for the static category colors/labels; it's cached in-memory on the client.

   β”Œβ”€β”€β”€β”€ File (kept on client) ──────────────► <img src=blob:…>
   β”‚                                            (canvas + redaction)
   β”‚
   β”‚            β”Œβ”€β”€β”€ @gradio/client.predict("/anonymize_screenshot")
   File ─────────   FileData (multipart)
                β”‚   ────────────────────────►   PaddleOCR β–Ί GLiNER2
                β”‚                              ◄──────────────
                └── JSON: {boxes, spans, text, width, height}

Theme mapping

The carefully tuned colors from the original Ranymizer are preserved as CSS variables in :root and mapped to shadcn's semantic names via @theme inline in app.css:

original tokenshadcn name (Tailwind class)
--bg--background (bg-background)
--surface--card (bg-card)
--surface2--muted, --secondary
--text--foreground
--text2--muted-foreground
--accent--primary (bg-primary)
--accent-dim--accent (the shadcn one)
--border-c--border
--danger--destructive

Fonts (Inter, Lora, ui-monospace) are bound to --font-sans, --font-serif, --font-mono and exposed as font-sans, font-serif, font-mono Tailwind utilities.

Both dark (default) and the original light-mode media query are wired up; toggle via mode-watcher if you want a manual switch.

Environment knobs (Python)

vardefaultmeaning
USE_GPU1flip to 0 to force CPU
GLINER_MODELfastino/gliner2-privacy-filter-PII-multioverride to test other GLiNER2 checkpoints
GLINER_COMPILE1torch.compile on the GLiNER2 encoder (GPU only)
INFERENCE_DTYPEfloat32passed to PaddleOCR's engine_config.dtype
LOAD_MODELS_ON_STARTUP1preload both models so the first request is fast

Known limits

  1. Personnummer / orgnr recall will be soft out of the box. The GLiNER2-PII encoder was trained on English + Dutch / German / French / Italian / Spanish / Portuguese, never on Swedish national-ID formats. Description hints in PII_LABELS help but don't replace fine-tuning. Run an eval on real docs and expect to LoRA-tune.

  2. PaddleOCR returns line polygons, not per-word boxes. Each PII span is mapped to the OCR line(s) it overlaps; single-line spans are narrowed by character ratio within the line.

  3. Dependency interaction (Python side). PaddleOCR 3.5's transformers backend pins transformers >= 5.4.0. GLiNER2 has its own transformers requirement via gliner2[local]. If pip can't resolve, install gliner2[local] first then paddleocr==3.5.0 last in a fresh venv.

Architecture β€” one codebase, two build targets

This is not two apps. The redaction editor (state.svelte.ts + components) depends only on an AnonymizerEngine interface (src/lib/engine/). Two implementations, selected at build time by VITE_ENGINE:

TargetVITE_ENGINEInferencePrivacyShips as
Showcasegradio (default)Python gr.Server (ZeroGPU)image uploadedHF Space (static SPA)
Desktoplocaltransformers.js + ONNX Runtime Web + WebGPU, in a Web Workernothing leaves the deviceTauri 2.0 app
src/lib/engine/
β”œβ”€β”€ types.ts     # AnonymizerEngine interface β€” the only seam
β”œβ”€β”€ index.ts     # picks engine from import.meta.env.VITE_ENGINE
β”œβ”€β”€ gradio.ts    # showcase: wraps @gradio/client (api.ts)
β”œβ”€β”€ local.ts     # desktop: drives the worker
β”œβ”€β”€ worker.ts    # transformers.js OCR + PII (ES module worker)
β”œβ”€β”€ webgpu.ts    # auto β†’ webgpu β†’ wasm backend detection
└── models.ts    # model registry + offline category meta
src-tauri/        # Tauri 2.0 shell (per the sveltekit-tauri skill)

Run

# Showcase (unchanged): Python + Gradio engine
python server.py
cd frontend && bun run dev            # http://localhost:5173

# Secure local desktop (Tauri 2.0, local engine)
cd frontend
bun install
bunx tauri icon path/to/logo.png      # one-time: generate app icons
bun run tauri dev                     # hot-reloading desktop window
bun run tauri build                   # release bundle

bun run dev:local / bun run build:local build the SPA with the local engine without the Tauri shell (browser testing).

Security (desktop)

  • No Rust commands β€” the whole pipeline is in the WebView; zero native attack surface. capabilities/default.json grants core:default only (no fs/shell/http/dialog).
  • Strict CSP in tauri.conf.json: connect-src is the only outbound allowance and only reaches the HF Hub β€” for the first-run model download, cached in the WebView thereafter. The image/text are never in an outbound request. Bundle weights as resources + drop connect-src to go fully air-gapped.
  • The source image still never round-trips a server in any mode (createObjectURL); the showcase only uploads to ZeroGPU for inference.

⚠️ Open item β€” model parity (the real R&D)

src/lib/engine/models.ts ships placeholder model ids. The Python side is PaddleOCR (full-page OCR with line polygons) + GLiNER2 (label-conditioned PII). Neither runs in transformers.js as-is: in-browser OCR that returns box geometry, and a GLiNER2-equivalent ONNX (Swedish personnummer/orgnr), are unresolved β€” worker.ts wires the pipeline correctly but box placement depends on this. Tracked alongside "Known limits" above.

Tooling note: the existing project's Vite/Tailwind/shadcn setup was kept as-is; only the Tauri parts of the local sveltekit-tauri skill were applied (it otherwise bootstraps a greenfield rolldown/Biome stack).

Contributors

Borg93

48 commits

Borg93/ranymizer

Python

0

48 commits

updated Jun 11, 2026

See the code

README


title: Ranymizer emoji: πŸ”’ colorFrom: indigo colorTo: purple sdk: gradio sdk_version: 6.14.0 app_file: backend/server.py pinned: true license: gpl-3.0

Ranymizer β€” Swedish screenshot redactor

OCR β†’ PII detection β†’ canvas editor for redacting Swedish screenshots before sharing. Two build targets from one codebase:

  • Showcase (Python gr.Server backend, ZeroGPU on HF Space): PaddleOCR 3.5 β†’ GLiNER2-PII β†’ JSON.
  • Desktop (Tauri 2.0, fully local): transformers.js β†’ ONNX Runtime Web β†’ WebGPU/WASM, no backend.

Stack: SvelteKit 2 Β· Svelte 5 (runes) Β· Tailwind 4 Β· shadcn-svelte Β· Bun Β· Tauri 2 Β· Biome.

.
β”œβ”€β”€ README.md
β”œβ”€β”€ Dockerfile                # showcase: Python + static SPA, EXPOSE 7860
β”œβ”€β”€ Makefile                  # `make install`, `make showcase-backend`, `make tauri-dev`, …
β”œβ”€β”€ example-images/           # (you create) drop test screenshots here for the showcase landing page
β”œβ”€β”€ backend/                  # Python β€” showcase target only
β”‚   β”œβ”€β”€ pyproject.toml        # uv project (name = "ranymizer")
β”‚   β”œβ”€β”€ .python-version       # 3.11
β”‚   β”œβ”€β”€ requirements.txt      # fallback for hosts that need pip
β”‚   β”œβ”€β”€ app.py                # PaddleOCR + GLiNER2 pipeline
β”‚   β”œβ”€β”€ server.py             # gr.Server: API routes + SvelteKit static mount
β”‚   └── README.md             # uv setup + run instructions
└── frontend/                 # SvelteKit 2 SPA β€” both targets
    β”œβ”€β”€ package.json          # bun-managed
    β”œβ”€β”€ biome.json            # formatter + linter (.ts / .json only β€” Svelte handled by svelte-check)
    β”œβ”€β”€ svelte.config.js      # adapter-static (right for BOTH targets)
    β”œβ”€β”€ vite.config.ts        # engine selected by VITE_ENGINE at build time
    β”œβ”€β”€ src-tauri/            # Tauri 2.0 shell (locked-down CSP, core:default capability only)
    └── src/
        β”œβ”€β”€ app.html / app.css
        β”œβ”€β”€ routes/           # +layout.ts has ssr=false + prerender=true (SPA)
        └── lib/
            β”œβ”€β”€ state.svelte.ts   # EditorState (depends on engine, not on Gradio)
            β”œβ”€β”€ api.ts            # @gradio/client wrapper (showcase only)
            β”œβ”€β”€ utils.ts          # cn() + shadcn-svelte type helpers
            β”œβ”€β”€ types.ts
            β”œβ”€β”€ engine/           # ← the seam between UI and inference
            β”‚   β”œβ”€β”€ types.ts      #   AnonymizerEngine interface
            β”‚   β”œβ”€β”€ index.ts      #   picks engine from import.meta.env.VITE_ENGINE
            β”‚   β”œβ”€β”€ gradio.ts     #   showcase: wraps @gradio/client
            β”‚   β”œβ”€β”€ local.ts      #   desktop: drives the worker
            β”‚   β”œβ”€β”€ worker.ts     #   transformers.js OCR + PII (ES module worker)
            β”‚   β”œβ”€β”€ webgpu.ts     #   auto β†’ webgpu β†’ wasm backend detection
            β”‚   └── models.ts     #   model registry + offline category meta
            └── components/
                β”œβ”€β”€ ui/       # shadcn-svelte primitives (button, card, toggle-group, badge, separator, toggle)
                β”œβ”€β”€ Landing.svelte / Editor.svelte / Canvas.svelte / Sidebar.svelte / Loading.svelte

Setup (one-time)

make install     # installs bun + rust + uv if missing, then frontend deps + backend venv

Manual install:

# bun (frontend), rust (Tauri), uv (backend Python)
curl -fsSL https://bun.sh/install | bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
curl -LsSf https://astral.sh/uv/install.sh | sh

cd frontend && bun install
cd backend  && uv sync

Run

The Makefile has every target β€” make (with no args) lists them. The common ones:

# ── Showcase (Python backend + SPA over network) ──────────────
make showcase-backend      # Python gr.Server on :7860
make showcase-frontend     # SvelteKit dev on :5173 (Gradio engine)

# ── Desktop (Tauri 2.0, fully local, no Python) ──────────────
make tauri-dev             # opens a native window, hot-reload
make tauri-build           # release bundle (.dmg/.app/.deb/.msi)

# ── Local engine in a browser (no Python, no native shell) ───
make local-dev             # http://localhost:5173 with VITE_ENGINE=local
make local-build           # static build with VITE_ENGINE=local

# ── Showcase in one Docker image ─────────────────────────────
make docker-build && make docker-run    # http://localhost:7860

CORS is open on the Python side for :5173 so the dev frontend can hit the backend on :7860. Edits to .svelte / .ts hot-reload. Edits to app.py or server.py require a Python restart.

Data flow

Same pattern as the BiRefNet demo: FileData in, JSON out. The source image is never round-tripped through the server β€” the SvelteKit client already has the File, so state.upload(file) calls URL.createObjectURL(file) and feeds it straight to <img>. The Python side returns only {filename, width, height, boxes, text, spans}. /api/meta is a separate plain GET for the static category colors/labels; it's cached in-memory on the client.

   β”Œβ”€β”€β”€β”€ File (kept on client) ──────────────► <img src=blob:…>
   β”‚                                            (canvas + redaction)
   β”‚
   β”‚            β”Œβ”€β”€β”€ @gradio/client.predict("/anonymize_screenshot")
   File ─────────   FileData (multipart)
                β”‚   ────────────────────────►   PaddleOCR β–Ί GLiNER2
                β”‚                              ◄──────────────
                └── JSON: {boxes, spans, text, width, height}

Theme mapping

The carefully tuned colors from the original Ranymizer are preserved as CSS variables in :root and mapped to shadcn's semantic names via @theme inline in app.css:

original tokenshadcn name (Tailwind class)
--bg--background (bg-background)
--surface--card (bg-card)
--surface2--muted, --secondary
--text--foreground
--text2--muted-foreground
--accent--primary (bg-primary)
--accent-dim--accent (the shadcn one)
--border-c--border
--danger--destructive

Fonts (Inter, Lora, ui-monospace) are bound to --font-sans, --font-serif, --font-mono and exposed as font-sans, font-serif, font-mono Tailwind utilities.

Both dark (default) and the original light-mode media query are wired up; toggle via mode-watcher if you want a manual switch.

Environment knobs (Python)

vardefaultmeaning
USE_GPU1flip to 0 to force CPU
GLINER_MODELfastino/gliner2-privacy-filter-PII-multioverride to test other GLiNER2 checkpoints
GLINER_COMPILE1torch.compile on the GLiNER2 encoder (GPU only)
INFERENCE_DTYPEfloat32passed to PaddleOCR's engine_config.dtype
LOAD_MODELS_ON_STARTUP1preload both models so the first request is fast

Known limits

  1. Personnummer / orgnr recall will be soft out of the box. The GLiNER2-PII encoder was trained on English + Dutch / German / French / Italian / Spanish / Portuguese, never on Swedish national-ID formats. Description hints in PII_LABELS help but don't replace fine-tuning. Run an eval on real docs and expect to LoRA-tune.

  2. PaddleOCR returns line polygons, not per-word boxes. Each PII span is mapped to the OCR line(s) it overlaps; single-line spans are narrowed by character ratio within the line.

  3. Dependency interaction (Python side). PaddleOCR 3.5's transformers backend pins transformers >= 5.4.0. GLiNER2 has its own transformers requirement via gliner2[local]. If pip can't resolve, install gliner2[local] first then paddleocr==3.5.0 last in a fresh venv.

Architecture β€” one codebase, two build targets

This is not two apps. The redaction editor (state.svelte.ts + components) depends only on an AnonymizerEngine interface (src/lib/engine/). Two implementations, selected at build time by VITE_ENGINE:

TargetVITE_ENGINEInferencePrivacyShips as
Showcasegradio (default)Python gr.Server (ZeroGPU)image uploadedHF Space (static SPA)
Desktoplocaltransformers.js + ONNX Runtime Web + WebGPU, in a Web Workernothing leaves the deviceTauri 2.0 app
src/lib/engine/
β”œβ”€β”€ types.ts     # AnonymizerEngine interface β€” the only seam
β”œβ”€β”€ index.ts     # picks engine from import.meta.env.VITE_ENGINE
β”œβ”€β”€ gradio.ts    # showcase: wraps @gradio/client (api.ts)
β”œβ”€β”€ local.ts     # desktop: drives the worker
β”œβ”€β”€ worker.ts    # transformers.js OCR + PII (ES module worker)
β”œβ”€β”€ webgpu.ts    # auto β†’ webgpu β†’ wasm backend detection
└── models.ts    # model registry + offline category meta
src-tauri/        # Tauri 2.0 shell (per the sveltekit-tauri skill)

Run

# Showcase (unchanged): Python + Gradio engine
python server.py
cd frontend && bun run dev            # http://localhost:5173

# Secure local desktop (Tauri 2.0, local engine)
cd frontend
bun install
bunx tauri icon path/to/logo.png      # one-time: generate app icons
bun run tauri dev                     # hot-reloading desktop window
bun run tauri build                   # release bundle

bun run dev:local / bun run build:local build the SPA with the local engine without the Tauri shell (browser testing).

Security (desktop)

  • No Rust commands β€” the whole pipeline is in the WebView; zero native attack surface. capabilities/default.json grants core:default only (no fs/shell/http/dialog).
  • Strict CSP in tauri.conf.json: connect-src is the only outbound allowance and only reaches the HF Hub β€” for the first-run model download, cached in the WebView thereafter. The image/text are never in an outbound request. Bundle weights as resources + drop connect-src to go fully air-gapped.
  • The source image still never round-trips a server in any mode (createObjectURL); the showcase only uploads to ZeroGPU for inference.

⚠️ Open item β€” model parity (the real R&D)

src/lib/engine/models.ts ships placeholder model ids. The Python side is PaddleOCR (full-page OCR with line polygons) + GLiNER2 (label-conditioned PII). Neither runs in transformers.js as-is: in-browser OCR that returns box geometry, and a GLiNER2-equivalent ONNX (Swedish personnummer/orgnr), are unresolved β€” worker.ts wires the pipeline correctly but box placement depends on this. Tracked alongside "Known limits" above.

Tooling note: the existing project's Vite/Tailwind/shadcn setup was kept as-is; only the Tauri parts of the local sveltekit-tauri skill were applied (it otherwise bootstraps a greenfield rolldown/Biome stack).

Contributors

Borg93

48 commits

Languages

Python

51.1%

Svelte

29.5%

TypeScript

15.4%

CSS

1.8%

Rust

1.0%