- JavaScript 41.3%
- Python 33%
- HTML 12.6%
- CSS 12.1%
- PowerShell 0.7%
- Other 0.3%
|
|
||
|---|---|---|
| app | ||
| deploy | ||
| static | ||
| templates | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| README.md | ||
| requirements.txt | ||
| run-dev.ps1 | ||
| run-preview.ps1 | ||
| schema.sql | ||
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
ProxyPasstargets it, so use a DHCP reservation / static IP (or a LAN-only/etc/hostsalias 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) toDATA_DIR/models, cached on the volume thereafter. Bigger models = more accurate + slower;baseon CPU handles a 5-min clip in ~1–3 min (much faster on a GPU viaWHISPER_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
MediaRecorderwebm support is limited. - Thumbnails are captured client-side at stop — no server-side ffmpeg needed.
- Storage grows with usage on the
loom_datavolume; prune old videos from the UI.