- TypeScript 82.9%
- JavaScript 6.5%
- Python 3.5%
- Go 3.4%
- CSS 1.9%
- Other 1.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Four things had gone stale and several features were missing entirely. Corrected: `/` is an entry point that redirects (admin default -> sole active room -> `/rooms`), not the directory; `/rooms` is the directory and `/r/<slug>` a room overview. The `/admin` console described here no longer exists — song administration lives at `/songs`, `/admin` is the server-settings page. The player no longer has an "arming click": it starts muted and playing with an unmute control, because a modal in front of the show was the wrong answer to autoplay policy. Added: the song library index, bulk upload, MP3+CDG disc ingest, the self-hosted separation/transcription worker, and casting in both modes. Each says what it costs — the disc path emits placeholder words, the self-hosted path is flagged for review, and casting to a speaker will not put lyrics on the TV. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
| .github | ||
| .scratch | ||
| backups | ||
| cast | ||
| db | ||
| docs | ||
| e2e | ||
| ingest-worker | ||
| mcp | ||
| observability | ||
| packages | ||
| patches | ||
| proxy | ||
| scripts | ||
| tests/magnitude | ||
| web | ||
| worker | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitignore | ||
| .npmrc | ||
| .template-migrations | ||
| AGENTS.md | ||
| biome.json | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| compose.dev.yaml | ||
| compose.external-edge.yaml | ||
| compose.prod.yaml | ||
| compose.yaml | ||
| CONTEXT.md | ||
| DESIGN.md | ||
| example.env | ||
| LICENSE | ||
| lighthouserc.cjs | ||
| package.json | ||
| playwright.config.ts | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| STANDARDS.md | ||
mune
A self-hostable karaoke night app for a venue — one stage or many rooms, each with its own live queue, host, and interlude music. Admins upload songs and run the show; singers browse a shared library (hybrid full-text + vector search), queue themselves in the key they sing (the player really pitch-shifts the instrumental), and optionally tip for priority; a big-screen player window renders synchronized word/syllable lyrics over the instrumental. Everything runs on infrastructure you own — Next.js in front, SurrealDB as the database and GraphQL API and token authorizer, Docker Compose around it. No SaaS is required to run the baseline (Music.ai, Stripe, and the embedding provider are optional, env-gated extras).
The three flows
- Singer (any phone, no install, no password): open the app → you're a guest instantly → browse/search Songs → pick one (with an optional key shift — the app remembers what you usually sing, and the player actually shifts the pitch) → give a stage name → you're in the live Queue. Your first song of the night is free; from the second (within a 12 h "tonight" window) the app asks you to secure your account (email code → passkey) and enable notifications — web push is how the venue calls you to the stage ("you're up next" / "you're up now").
- Admin (staff, granted via
pnpm admin:grant <email>): /songs is the song library — every song sorted by what needs a human, each row carrying its status and, for a refused ingest, the reason, one tap from the editor. Add songs one at a time or in bulk (drop a pile of files; MP3+CDG pairs are matched by filename and nothing you selected is ever dropped silently), ingest them through Music.ai, a karaoke disc, or a self-hosted pipeline, and retime them in the WYSIWYG lyrics editor. /admin is the server-settings page — room-creation policy, the default room, and the embeddings backfill. Capability is ADDITIVE, never a parallel UI: a room host drives their night from the room's own queue (/r/<room>/queue) — the same page every singer sees, which simply grows the host's controls: drag to reorder (a one-time nudge — pinning is a separate explicit toggle), transport (play / pause / skip), a per-room interlude playlist, and a share-this-room link. - Player window (the venue's big screen): open /r//player full-screen. It follows the room's playback state live — pitch-shifted instrumental audio, the active word highlighted syllable-by-syllable, a break-cue countdown through instrumental gaps, interlude music between singers, and an ambient canvas visualization behind a blurred lyrics panel. When a song's audio ends it auto-advances to the next singer (paused). It starts muted and playing — browsers refuse un-gestured audible autoplay, so an unmute control sits at the bottom until you tap it, and never a modal in front of the show. The app nav stays unless you take it fullscreen. From here you can also cast: audio to a speaker (follows the queue, lyrics stay on this screen) or a rendered video to a TV (lyrics on the TV, one song).
Implemented features
Baseline (always on, exercised by the e2e suite):
- Anonymous-first identity — guests own real data; upgrade = email OTP → required passkey. No passwords. (docs/auth.md)
- Admin role — one
rolefield on the user, granted operationally withpnpm admin:grant <email>(--revoketo undo); enforced by server guards and row-levelPERMISSIONS($token.role) in the db. - Multi-room —
roomis a first-class entity owning its queue, playback, and interlude music; the song library, verified flags, and embeddings stay global. Path-scoped/r/<slug>/…(the slug IS the URL)./roomsis the directory and always lists;/is only an entry point, redirecting to the admin-set default room, else the sole active room, else the directory./r/<slug>is the room's overview — Queue and Player buttons, live now-playing, and tonight's statistics. Every stage surface lives under its room — there is no venue-level queue or player. - Song library (
/r/<room>/songs) — one search box with fused ranked results: SurrealDB full-text (edgengram prefix) + a jaro-winkler typo leg, plus a vector leg when embeddings are configured (below); the room-targeted add flow; processing chips + "verified good" badges. - Queue self-add — key-shift picker (−3…+3, recommendation from your
history; the player really shifts the audio, below), a required stage
name, and the per-night gate: the first song of a 12 h "tonight"
window is free; the second requires account → passkey → ≥ 1 push
subscription (
push_required— skipped on keyless deployments). Server-side priority scoring (1000 + tip_cents × 2 − recent × 250). - Live queue (
/r/<room>/queue) — room-filtered GraphQL subscriptions, ordered by priority then arrival; own-entry highlight and removal (a status write that crosses every open context live — never a row DELETE); per-singer "first song tonight" / "N songs tonight" badges. Rows are venue-public, but tips are fully private (field-level PERMISSIONS — owner or server admin only; no public boost trace). - Directory & room overview (
/rooms,/r/<room>) — the directory lists every room with its host, live now-playing line and waiting count, marking the default room and the one you are in; the overview gives a room's now-playing hero, derived up-next, and tonight's counts over the same 12 h window as the priority penalty. Navigation is contextual: an odd-count nav (3 or 5) that keeps Queue dead-centre, with the room's own name as the first item once you are in one. - Player (
/r/<room>/player) — big-screen stage, keeping the app nav with an opt-in fullscreen control that hides it; follows the room'splayback_stateover a live query; rAF word/syllable highlighting from timedlyrics_json; real pitch shifting; a break-cue countdown through gaps; interlude rendering; canvas visualization; reduced-motion aware. - Roles & host controls — server admin (
role = 'admin', operationally granted) vs room host (the room's creator). Admin capability is ADDITIVE, not a parallel UI:/r/<room>/queuegrows DnD reorder (a one-time nudge — never auto-pins), an explicit per-row pin toggle, transport (play / pause / start over / back 10s / skip — a restart writes position 0 through the sameseekthe cast stream follows), the interlude panel and a share-this-room link for a host or admin, and nothing at all for anyone else./songsis the admin library and/adminthe server-settings page; there is no separate console. - Song library index (
/songs, admin) — every song ordered by what needs a human (awaiting review, awaiting lyrics, failed, refused, processing, ready) with search, a status filter, the refusal reason on the row, and the whole row linking into that song's editor. - Bulk upload — select or drop many files at once;
.mp3/.flac/.wav/.m4a/.oggpair with.cdgby filename stem, metadata is read from tags then the filename and shown in an editable table before anything is created, uploads run capped-concurrent, and one failure never loses the batch. Anything unpaired or unusable is reported with its reason — nothing you selected disappears quietly. - MP3 + CDG karaoke discs — drop a commercial disc pair and the media worker decodes the CD+G subcode itself (no OCR, no cloud): it finds the lyric bands, classifies the palette spatially (never by brightness), and reads word timings off the highlight sweep — measured at a 0.33 ms pooled median against the disc's own instruction stream. Phase 1 emits those timings with placeholder words for an operator to type against the disc's own picture of each line; a disc it cannot time honestly is refused with the reason, not silently mistimed.
- WYSIWYG lyrics editor — the player's own karaoke rendering, synced to a stem, everything editable in place: click-a-word seek, an inspector (text + start/end steppers + stamp-at-playhead), and tap-to-time retiming — word- OR syllable-granular. Save re-validates and re-embeds.
- Media store — files on disk under
MEDIA_DIR, streamed by/api/media/[name]with HTTP Range support (audio seeking); server-named UUID files; resumable tus uploads with real progress (@tus/server, staging dir + atomic rename; size/extension caps). - Venue activity feed (
/r/<room>/activity) — live room events (queued / now-playing / secured) ∪ the caller's own, plus the web-push opt-in. - Offline outbox — the activity composer writes through the IndexedDB outbox (pending/failed chips, cross-tab single drainer, Background Sync).
- Demo night out of the box —
pnpm db:seedsynthesizes playable demo WAVs and seeds two hand-timed public-domain songs + queue rows, so the full loop (queue → play → lyrics) works with no external service. - Template platform — PWA (installable, offline fallback), typed i18n catalog, OKLCH theming with per-user personalization, typed env boundaries, four-layer test rig, one-command prod compose stack.
Env-gated (off ⇒ the feature is invisible and its routes answer 503; see docs/karaoke.md for setup):
- Music.ai song processing — upload an original, get stems + timed
lyrics ingested via webhook or the dashboard re-check button. Flags:
MUSICAI_API_KEY,MUSICAI_WORKFLOW,MUSICAI_WEBHOOK_SECRET. Without a key, manual mode is the always-working baseline. - Room creation —
ROOM_CREATION_ENABLED(default off) is the deployment default for whether any enrolled account may create a room; off ⇒ server-admin-only creation. A server admin flips the policy at runtime from/admin— the server settings page (stored setting ?? env default) — and creates rooms from the venue directory/roomsregardless of it. The /admin refusal surface doubles as a growth pitch — "create your own room" (enabled) or "host your own instance" with the AGPL-3.0 source link from build-timeNEXT_PUBLIC_SOURCE_URL(disabled). A room is created via a server command, thenactive → archived, never deleted. - Stripe tipping ("Boost") — preset tip amounts via Stripe Checkout;
the webhook credits
tip_centsand recomputes priority (pinned rows keep their spot). Tips are fully private — the only trace is a tip-received push to the tipper. Flags:STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,TIPS_CURRENCY,TIPS_PRESETS_CENTS. - Web push stage calls — "you're up next" / "you're up now",
admin song-processing pushes, and a private tip-received push, all
through the VAPID web-push channel (push replaced SMS). Also the third
leg of the second-song gate (
push_required). Flags:PUSH_VAPID_PUBLIC,PUSH_VAPID_PRIVATE,PUSH_SUBJECT— keyless degrades loudly (both the stage-call channel and the gate leg are off, and the/admindashboard says so). - Hybrid vector search — with an embedding provider configured, the
song search adds a SurrealDB HNSW vector leg fused (
search::rrf) with the always-on full-text + typo legs; keyless deployments degrade to full-text only (never error). Backfill from the/adminEmbeddings card. Flags:EMBEDDING_PROVIDER(vllm/google/openai-compatible/none),EMBEDDING_MODEL,EMBEDDING_BASE_URL,EMBEDDING_API_KEY,EMBEDDING_DIMS. (docs/embeddings.md) - ffmpeg video export — the
render-workercompose service (--profile render) burns ASS karaoke subtitles + a generated background + the chosen stem into a downloadable.mp4. Knobs:RENDER_POLL_MS,RENDER_STALL_TIMEOUT_MS. (docs/rendering.md) - Self-hosted song processing — an opt-in
ingest-worker(--profile ingest) that separates and transcribes on your own CPU, with no cloud and no per-song cost: Mel-Band RoFormer for the backing track andfaster-whisper large-v2for the words, all MIT-licensed weights. Measured against the incumbent on the same songs it reaches parity on one and ~1.5 points behind on a call-and-response track, and its instrumental beats the incumbent's on both. Every result is flagged for review before it can reach the stage; a bad alignment is refused by a validatedctc_scorefloor rather than shipped. ~4–7 CPU-min/song. - Casting (
CAST_ENABLED,CAST_PUBLIC_URL,CAST_TOKEN_SECRET,--profile cast) — two modes, each stating its cost, neither a default. Audio to a speaker via Sendspin: a Go service follows the room's playback state (the database stays authoritative, so closing the player tab does not silence the TV and pausing the room pauses the cast) and the lyric cursor runs off its clock. Video to a TV: the pre-rendered MP4 with lyrics burned into the frames, played by Google's stock receiver — lyrics on the TV, but one song, not the queue. A Chromecast carries no session, so both use an HMAC capability token in the URL path, and the two token kinds are domain-separated so neither can be spent as the other. - Matrix sign-in — optional OIDC against a configured homeserver
(
MATRIX_OIDC_*), inherited from the template.
Deliberately not implemented (recorded with reasons in docs/open-questions.md): SIWE, Uppy (tus is used instead), Lottie assets, per-room song libraries / room theming / private rooms / room un-archive, host-scoped uploads, client-rendered room QR codes, and the buffer-mode pitch graph (shape B — the live-input path shipped).
Quickstart (dev)
Requires Node ≥ 24, pnpm 11, and Docker (for SurrealDB + Mailpit).
pnpm install
pnpm dev # compose stack (SurrealDB + Mailpit) + Next dev server
pnpm db:seed # demo songs (synthesized WAVs + timed lyrics), demo queue
Open http://localhost:3200 — the WEB_PORT this checkout's .env
assigns (each generated app gets its own contiguous port block; Mailpit's UI,
where OTP emails land in dev, is at MAILPIT_UI_PORT = http://localhost:3202).
The app boots you as a guest — pick a song and you're in the queue. To see
the admin side: secure your account on /account, then
pnpm admin:grant your@email and reload.
Verify a change before you commit:
pnpm check # Biome lint + typecheck + unit tests — must be green
pnpm e2e # Playwright against the compose stack
Documentation
Start with AGENTS.md — the operating manual (also symlinked
as CLAUDE.md). The binding contracts: CONTEXT.md (domain
language), STANDARDS.md (code), DESIGN.md
(design system), docs/adr/ (decision records).
Topic guides in docs/:
| Guide | Covers |
|---|---|
| karaoke.md | Running a karaoke night — granting admin, multi-room, song ingestion, the show, tips & push setup. |
| embeddings.md | Hybrid-search embedding providers (vLLM/Octen, Gemini), dims/normalization, the provider-switch re-embed. |
| rendering.md | The ffmpeg export worker (--profile render). |
| open-questions.md | Recorded product decisions & deviations awaiting human review. |
| auth.md | Anonymous-first auth, the SurrealDB bridge, the Matrix recipe. |
| database.md | SurrealDB schema layout, GraphQL, sync/seed. |
| serwist.md | Service worker, caching policy, manifest/icons/install. |
| observability.md | The reporter, structured logging, OTLP/OpenObserve, RUM. |
| testing.md | The four-layer test rig and the CI job map. |
| deployment.md | Zero-to-HTTPS production on a VPS. |
| operations.md | Backups (the media dir needs its own!), restore drills, upgrades. |
| maintenance.md | Keeping every pin current. |
| template-updates.md | Pulling template improvements into this app; migrations. |
| releases.md | Cutting semver releases; what each bump promises. |
Template origin
mune was generated from the Mune base template (create-mune) — a
batteries-included, entirely self-hostable PWA monorepo: anonymous-first auth,
an offline outbox, live queries, push, an OKLCH design system, and a
one-command production stack. The karaoke feature set was built on top of that
working baseline; the template's demo pages (notes/board) were replaced by the
karaoke surfaces.
This app stays connected to its template: pull later template improvements
with pnpm template:update --from <template checkout or URL> --ref latest —
three-way merged into your tree, with codemod migrations for breaking changes
(docs/template-updates.md). Releases are
semver-tagged (docs/releases.md).
The create-mune scaffolder is MIT-licensed. This app is distributed under the
GNU AGPL-3.0 (LICENSE at the repo root) — §13 requires offering
the running source to network users, which is why the "host your own instance"
growth pitch links to NEXT_PUBLIC_SOURCE_URL; point it at your fork.