No description
  • JavaScript 41.3%
  • Python 33%
  • HTML 12.6%
  • CSS 12.1%
  • PowerShell 0.7%
  • Other 0.3%
Find a file
2026-08-08 15:40:13 -04:00
app Derive recording length from the WebM itself + backfill existing rows 2026-08-08 15:36:23 -04:00
deploy Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
static Fix wrong recording length (e.g. an 8:12 video listed as 1:40) 2026-08-08 15:27:44 -04:00
templates Fix wrong recording length (e.g. an 8:12 video listed as 1:40) 2026-08-08 15:27:44 -04:00
.dockerignore Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
.env.example Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
.gitignore Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
docker-compose.yml Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
Dockerfile Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
LICENSE Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
README.md Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
requirements.txt Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
run-dev.ps1 Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
run-preview.ps1 Lumo — self-hosted, in-browser screen recorder 2026-07-07 12:00:00 +00:00
schema.sql Add user-defined permalinks for recordings 2026-07-18 19:41:08 -04:00

Lumo (Algocrafts™)

Lumo is a self-hosted Loom-style screen recorder. Record your screen (with an optional webcam bubble) and microphone in the browser, get an instant shareable link, and let the team watch, react, and leave timestamped comments — organised into (nested) folders.

Light and dark mode, toggled from the top bar.

Nothing to install for users: recording uses the browser's built-in getDisplayMedia / MediaRecorder APIs. Sign-in is Forgejo SSO.

Features

  • 🎥 Record Screen + Cam (webcam composited as a circular bubble), Screen only, or Cam only
  • 🎙️ Microphone and/or system-audio capture, with pause/resume
  • 🤖 Auto-frame — on-device face detection pans/zooms to keep you centred (Google-Meet-style)
  • 🖼️ Virtual backgrounds — Blur, plus animated Bokeh / Confetti / Constellation / Raindrops / Cosmos (segmentation cuts you out; e.g. Cosmos = twinkling stars + occasional meteors)
  • 🔗 Instant /v/{id} share link; Workspace-only or Public per video
  • 📁 Folders, My recordings / All recordings views
  • 📝 On-demand auto captions (local faster-whisper, on-device/offline) → WebVTT track with a CC toggle
  • Blur regions — owner/admin draws boxes over the player and gives each a time window; the player blurs (or blacks out) that area for that range. Overlay-only — hides it in the player without re-encoding the source
  • 💬 Timestamped comments (click a timestamp to seek) + 👍❤️😂🎉👀🔥 reactions
  • 👁️ View counts, inline title/description editing, delete
  • ⬆️ Chunked uploads (5 MB parts) so recordings sail past Cloudflare's 100 MB body limit
  • 🗄️ Zero external services: SQLite + files on a single Docker volume

Architecture

How an end user's request reaches the app:

End user (browser)
  │
  │  ① DNS lookup: lumo.<your-domain-name>.net  →  Domain registrar
  │     (returns the edge server's public IP)
  ▼
┌─ Edge server ────────────────────┐
│  Apache reverse proxy · TLS      │   ② HTTPS :443 (Let's Encrypt)
└──────────────────────────────────┘
  │
  │  ③ reverse-proxy over the LAN to :8080
  ▼
┌─ Origin server (Docker CE) ──────┐
│  Lumo app · FastAPI :8080        │   container `lumo`
│  SQLite + video files on /data   │   (big disk: /opt/lumo/data)
└──────────────────────────────────┘
  │
  │  ④ sign-in via Forgejo SSO (OAuth2):
  │     DNS lookup: forgejo.<your-domain-name>.net  →  Domain registrar
  │     browser redirect to authorize, then server-side token + user lookup
  ▼
┌─ Forgejo (SSO) ──────────────────┐
│  forgejo.<your-domain-name>.net  │   OAuth2 identity provider
└──────────────────────────────────┘

Tech

FastAPI · SQLite · Jinja templates · vanilla JS. Deps: fastapi uvicorn jinja2 httpx itsdangerous faster-whisper requests.

Camera effects run entirely in the browser via MediaPipe Tasks Vision (FaceDetector + selfie ImageSegmenter). The wasm + .tflite models are vendored under static/vendor/mediapipe/ (~19 MB, mostly the two wasm variants) so nothing is fetched from a CDN at runtime — they're served locally and cached by the browser after first use. GPU (WebGL) delegate with automatic CPU fallback. If a model can't load, that effect silently disables and the raw camera is used.

loom/
  app/        main.py (routes) · auth.py (Forgejo OAuth) · db.py · config.py
  templates/  base · login · dashboard · record · watch
  static/     css/app.css · js/{recorder,watch,dashboard,camfx}.js
  static/vendor/mediapipe/  vision_bundle.mjs · wasm/ · models/  (MediaPipe, vendored)
  schema.sql  Dockerfile  docker-compose.yml  .env.example  run-dev.ps1

Try it locally (no Forgejo needed)

cd loom
./run-dev.ps1        # http://localhost:8080  ->  "Continue as Dev User"

DEV_AUTH=1 enables a fake local login so you can record and watch immediately. Screen capture requires localhost or HTTPS (browsers block it on other hosts).

Deployment

0. Forgejo OAuth app

Forgejo → Settings → Applications → Manage OAuth2 Applications → Create:

  • Name: Lumo
  • Redirect URI: https://lumo.<your-domain-name>.net/auth/callback (exact)

Note the Client ID / Client Secret.

1. DNS

Point lumo.<your-domain-name>.net at the edge server — same target as the other *.<your-domain-name>.net sites.

2. App host (origin server) — Docker CE + /opt/lumo

Use Docker CE, not the Docker snap — the snap is confined and cannot read /opt:

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"          # re-login for the group to take effect
sudo mkdir -p /opt/lumo && sudo chown "$USER" /opt/lumo
git clone https://forgejo.<your-domain-name>.net/QuaderEnterprises/lumo.git /opt/lumo   # private repo: use a deploy key or a token
cd /opt/lumo
cp .env.example .env      # fill BASE_URL + FORGEJO_BASE_URL + OAUTH_CLIENT_ID/SECRET + SESSION_SECRET + ADMIN_LOGINS

Set LUMO_DATA_DIR in .env to a directory on the big data disk so videos / DB / Whisper models live there instead of a Docker named volume on the root disk:

LUMO_DATA_DIR=/opt/lumo/data
LUMO_PORT=8080

Then build + start and health-check:

docker compose up -d --build
curl -s localhost:8080/healthz          # -> {"ok": true}

docker-compose.yml bind-mounts ${LUMO_DATA_DIR}/data (falls back to a named volume if unset). The first caption run downloads the Whisper model (~150 MB) into $LUMO_DATA_DIR/models.

3. Edge server — Apache reverse proxy + TLS

Create the base vhost pointing at the origin server's LAN IP (template: deploy/apache-lumo.conf):

<VirtualHost *:80>
    ServerName lumo.<your-domain-name>.net
    ProxyPreserveHost On
    ProxyTimeout 600
    ProxyPass        / http://<origin-LAN-IP>:8080/ nocanon disablereuse=on
    ProxyPassReverse / http://<origin-LAN-IP>:8080/
    ErrorLog  ${APACHE_LOG_DIR}/lumo_error.log
    CustomLog ${APACHE_LOG_DIR}/lumo_access.log combined
</VirtualHost>
sudo a2enmod proxy proxy_http ssl rewrite
sudo a2ensite lumo.<your-domain-name>.net
sudo apache2ctl configtest && sudo systemctl reload apache2
sudo certbot --apache -d lumo.<your-domain-name>.net    # generates …-le-ssl.conf + HTTP->HTTPS redirect

4. Verify

curl -s https://lumo.<your-domain-name>.net/healthz     # {"ok": true}

Open the site → Sign in with Forgejo → record.

Updating

cd /opt/lumo && git pull && docker compose up -d --build

Operational notes

  • Pin the origin server's LAN IP — the Apache ProxyPass targets it, so use a DHCP reservation / static IP (or a LAN-only /etc/hosts alias on the edge server). Use the LAN IP, not Tailscale — the edge server reaches the origin server over the LAN.
  • Keep the origin server awake — if it's a workstation that suspends, the proxy 503s. Disable sleep: sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target.
  • Uploads are chunked (5 MB) so no reverse-proxy request-body cap is hit.

Environment variables

Var Purpose
BASE_URL Public URL, used for share links + default redirect URI
FORGEJO_BASE_URL Base URL of your Forgejo SSO instance (e.g. https://forgejo.<your-domain-name>.net)
OAUTH_CLIENT_ID / OAUTH_CLIENT_SECRET Forgejo OAuth app credentials
OAUTH_REDIRECT_URI Defaults to {BASE_URL}/auth/callback
SESSION_SECRET Signs session cookies — set a long random value
ADMIN_LOGINS Comma-separated Forgejo logins granted admin
DATA_DIR Where DB + videos live inside the container (default /data)
LUMO_DATA_DIR Host path bind-mounted to /data — put it on the big disk, off root (else a named volume is used)
LUMO_PORT Host port the container publishes (default 8080; the edge proxy targets this)
DEV_AUTH 1 enables the local fake login — never set in production
WHISPER_MODEL Caption model: tiny/base (default)/small/medium/large-v3
WHISPER_DEVICE cpu (default) or cuda
WHISPER_COMPUTE int8 (default, CPU) or float16 (GPU)
WHISPER_LANGUAGE Force a language code (default "" = auto-detect)

Auto captions

Captions are generated on demand — the owner clicks Generate captions on a video's page. Transcription runs locally via faster-whisper (nothing leaves the server; audio is decoded straight from the WebM by PyAV, no ffmpeg needed), writes a WebVTT file, and attaches it as a captions <track> with a CC toggle in the player.

  • First run downloads the model (~150 MB for base) to DATA_DIR/models, cached on the volume thereafter. Bigger models = more accurate + slower; base on CPU handles a 5-min clip in ~13 min (much faster on a GPU via WHISPER_DEVICE=cuda).
  • Transcriptions are serialised (one at a time) to bound CPU use. The Docker image installs libgomp1 (needed by ctranslate2/onnxruntime).

Notes / limits

  • Recordings are WebM (VP9/VP8 + Opus). Chrome/Edge/Firefox supported; Safari's MediaRecorder webm support is limited.
  • Thumbnails are captured client-side at stop — no server-side ffmpeg needed.
  • Storage grows with usage on the loom_data volume; prune old videos from the UI.