[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-80296":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":16,"stars7d":17,"stars30d":18,"stars90d":16,"forks30d":16,"starsTrendScore":19,"compositeScore":20,"rankGlobal":10,"rankLanguage":10,"license":21,"archived":22,"fork":22,"defaultBranch":23,"hasWiki":24,"hasPages":22,"topics":25,"createdAt":10,"pushedAt":10,"updatedAt":26,"readmeContent":27,"aiSummary":28,"trendingCount":16,"starSnapshotCount":16,"syncStatus":14,"lastSyncTime":29,"discoverSource":30},80296,"subwave","perminder-klair\u002Fsubwave","perminder-klair","Personal internet radio: Agentic AI DJ","https:\u002F\u002Fwww.getsubwave.com",null,"TypeScript",127,8,2,5,0,20,34,14,56.26,"MIT License",false,"main",true,[],"2026-06-11 04:07:04","# SUB\u002FWAVE\n\n**A personal internet radio station.** One Icecast stream, one broadcast.\nEvery listener hears the same thing at the same time. An AI DJ picks the\ntracks and talks between them: station idents, time checks, the weather,\na quick intro for whatever's going out next. You can ask for music in plain\nlanguage; the DJ works out what you meant and slots it in.\n\nIt's *radio*, not a playlist. No per-listener shuffle, no skip button, no\n\"up next for you.\" You tune in and hear whatever is on.\n\n## Live demo\n\n- **Project site** — [getsubwave.com](https:\u002F\u002Fwww.getsubwave.com\u002F)\n- **Demo player** — [getsubwave.com\u002Flisten](https:\u002F\u002Fwww.getsubwave.com\u002Flisten)\n- **Setup walkthrough** — [getsubwave.com\u002Fsetup](https:\u002F\u002Fwww.getsubwave.com\u002Fsetup)\n- **Operator manual** — [getsubwave.com\u002Fmanual](https:\u002F\u002Fwww.getsubwave.com\u002Fmanual)\n\n## Screenshots\n\n**The listener player.** One shared broadcast, with in-app song requests.\n\n\u003Cimg src=\"web\u002Fpublic\u002Fscreenshots\u002Flisten.webp\" alt=\"Player — the listener player on \u002Flisten\" width=\"640\">\n\n\u003Cimg src=\"web\u002Fpublic\u002Fscreenshots\u002Fplayer-request-song.webp\" alt=\"Player — request a song\" width=\"260\">\n\n**The admin console.** Where the operator runs the station.\n\n| | |\n|---|---|\n| \u003Cimg src=\"web\u002Fpublic\u002Fscreenshots\u002Fadmin-dash.webp\" alt=\"Admin — Dash: live status, queue, booth log\" width=\"100%\"> | \u003Cimg src=\"web\u002Fpublic\u002Fscreenshots\u002Fadmin-personas.webp\" alt=\"Admin — Personas: the DJ roster\" width=\"100%\"> |\n| **Dash** — live status, the queue, the booth log | **Personas** — the DJ roster, each with its own voice |\n| \u003Cimg src=\"web\u002Fpublic\u002Fscreenshots\u002Fadmin-shows.webp\" alt=\"Admin — Weekly schedule grid\" width=\"100%\"> | \u003Cimg src=\"web\u002Fpublic\u002Fscreenshots\u002Fadmin-skills.webp\" alt=\"Admin — Skills: what the DJ does between tracks\" width=\"100%\"> |\n| **Shows** — a 24×7 schedule you paint | **Skills** — what the DJ does between tracks |\n| \u003Cimg src=\"web\u002Fpublic\u002Fscreenshots\u002Fadmin-stats.webp\" alt=\"Admin — Stats: LLM and TTS usage\" width=\"100%\"> | \u003Cimg src=\"web\u002Fpublic\u002Fscreenshots\u002Fadmin-debug.webp\" alt=\"Admin — Debug: health, logs, LLM calls\" width=\"100%\"> |\n| **Stats** — LLM and TTS usage at a glance | **Debug** — health, logs, recent LLM calls |\n\n## Features\n\n- **One shared Icecast stream.** Every listener hears the same broadcast at the same time.\n- **AI DJ that picks and talks.** Curates tracks, writes intros, and reads station idents, the time, and the weather.\n- **Plain-language requests.** \"Play something more upbeat\" or \"anything by Radiohead\" works.\n- **Your own music library.** Pulls from Navidrome over the Subsonic API. No external catalogue.\n- **Swappable LLM provider.** Ollama, Anthropic, OpenAI, Google, DeepSeek, OpenRouter, Vercel AI Gateway, or any OpenAI-compatible server. Change it from the admin UI with no redeploy.\n- **Five TTS engines.** Piper and Kokoro in-process for fast local speech, plus an optional `tts-heavy` sidecar (`docker compose --profile tts-heavy up -d`) that adds Chatterbox (zero-shot voice cloning) and PocketTTS (6× real-time, EN\u002FFR\u002FDE\u002FIT\u002FES\u002FPT). Cloud (OpenAI \u002F ElevenLabs) is also available. Pick a different engine per kind of speech.\n- **Multiple DJ personas.** Up to 10 souls in rotation, each with its own voice and writing style.\n- **Dual-codec broadcast.** MP3 192 kbps for Sonos, hardware radios, and cars; Ogg-Opus 96 kbps for modern browsers. The web player picks automatically.\n- **PWA + terminal player.** Installable on phone and desktop with lock-screen controls, plus a TUI for the command line.\n- **Scheduled shows.** A 24×7 grid; each slot has its own persona, mood, and skills.\n- **Mood-aware rotation.** Time of day, weather, and festival days bias what gets played and how the DJ talks.\n- **Hourly archives.** Every hour saved as MP3 for later replay.\n- **Crossfade + voice ducking.** Tracks blend smoothly; the music ducks under DJ speech and lifts back up.\n- **Admin console.** Live status, queue, booth log, personas, shows, skills, stats, and a debug view of recent LLM calls.\n- **MCP server.** External agents (Claude Desktop, Cursor, etc.) can request songs and drive the DJ.\n- **Self-hosted.** One `docker compose up -d` on a single Linux host. Optional Cloudflare in front for TLS.\n\n## Why it's built this way\n\nA playlist is a list you control. Radio is a broadcast you join. SUB\u002FWAVE\nis the second kind:\n\n- **One shared stream.** A single Icecast mount everyone connects to.\n  Everyone hears the same audio at the same instant. That's what makes it\n  a station instead of a jukebox.\n- **No skip.** Track-end is the only natural transition. The DJ — human-curated\n  personas plus an LLM — owns the pacing, not the listener. (Operators *can*\n  skip via the admin API; listeners cannot.)\n- **AI as the DJ, not the catalogue.** The music is your own library, served\n  by Navidrome over the Subsonic API. The LLM picks what's next and talks\n  between tracks. It doesn't generate music and it doesn't replace your taste.\n- **Self-hosted and swappable.** Runs on one Linux box behind Cloudflare. The\n  LLM provider is swappable at runtime (Ollama, Anthropic, OpenAI, Google,\n  OpenRouter, Vercel AI Gateway) with no redeploy.\n\n## Architecture\n\nFour cooperating processes. The load-bearing design fact: the **controller**\nand **Liquidsoap** talk only through files in a shared `state\u002F` directory.\nThere is no socket or RPC channel between them.\n\n```\n                                  ┌───────────────────────────┐\n   Navidrome  ◀── Subsonic API ───│        Controller         │\n   (your music library)           │   (Node.js \u002F Express)     │\n                                  │                           │\n   LLM provider ◀── AI SDK ───────│  • AI DJ: picks tracks,   │\n   (Ollama \u002F Anthropic \u002F …)       │    writes links & idents  │\n                                  │  • session + scheduler    │\n   Open-Meteo ◀── weather ────────│  • text-to-speech (TTS)   │\n                                  │  • HTTP API (:7701)       │\n                                  └─────────────┬─────────────┘\n                                                │  file-based IPC\n                                   shared state\u002F │  next.txt · say.txt\n                                                │  intro.txt · auto.m3u\n                                                ▼  now-playing.json\n                                  ┌───────────────────────────┐\n                                  │        Liquidsoap         │\n                                  │  • queue → auto-playlist  │\n                                  │  • crossfade + voice duck │\n                                  │  • jingles, limiter       │\n                                  │  • encodes MP3 + Opus     │\n                                  └─────────────┬─────────────┘\n                                                │ source connect\n                                                ▼\n                                  ┌───────────────────────────┐\n                                  │   Icecast  (:7702)        │──▶ \u002Fstream.mp3\n                                  │                           │──▶ \u002Fstream.opus\n                                  └───────────────────────────┘\n                                                ▲\n   Browser \u002F PWA ◀── audio ───────────────────┘\n   (Next.js web UI :7700, polls controller for now-playing)\n\n   Production: Cloudflare ─HTTPS─▶ host :80 (Caddy) ─▶ web · controller · broadcast\n```\n\n### The four processes\n\n| Process | What it does |\n|---|---|\n| **Controller** (`controller\u002F`, Node\u002FExpress) | The brain. Runs the AI DJ: picks tracks, writes links\u002Fidents, matches listener requests, runs the cron scheduler, renders TTS. Exposes the HTTP API. |\n| **Broadcast** (`docker\u002FDockerfile.broadcast`) | One container, two processes. **Liquidsoap** (`liquidsoap\u002Fradio.liq`) is the mixing desk: request queue → auto-playlist, crossfades, ducks voice over music, mixes jingles, brick-wall limiter, encodes to MP3 (192 kbps) and Ogg-Opus (96 kbps) in parallel. **Icecast2** is the transmitter that serves both `\u002Fstream.mp3` and `\u002Fstream.opus` mounts to every listener; the web player picks Opus on browsers that support it and falls back to MP3 for Sonos, hardware radios, and older clients. A tiny supervisor entrypoint launches both and exits if either dies. |\n| **Web UI** (`web\u002F`, Next.js 15) | The receiver. Player, marketing landing page, setup walkthrough, and an admin shell for settings\u002Fdebug. PWA-installable with OS lock-screen controls. |\n\n### File-based IPC\n\nEverything that flows between the controller and Liquidsoap goes through one\nof these files in `state\u002F`:\n\n| File | Direction | Purpose |\n|---|---|---|\n| `next.txt` | controller → LS | Next annotated track URI. LS polls every 1.0s. |\n| `say.txt` | controller → LS | WAV path for a spoken segment (station ID, time, weather, request intro). Heavy-ducked over music. |\n| `intro.txt` | controller → LS | WAV path for a between-track DJ link. Light-ducked so the new song stays audible. |\n| `auto.m3u` | controller → LS | Fallback playlist for the current mood, rewritten hourly. |\n| `now-playing.json` | LS → controller\u002FUI | Current track metadata, written from Liquidsoap's metadata hook. |\n\nThe web UI polls the controller over HTTP (`\u002Fnow-playing`, `\u002Fstate` every 5s).\nBrowsers pull audio directly from Icecast.\n\n## Quick start (CLI — recommended)\n\n```bash\ncurl -fsSL https:\u002F\u002Fcli.getsubwave.com | sh    # installs, then offers to init + start\nsubwave setup                              # connect Navidrome + LLM\n```\n\nTwo Enter prompts during the installer (`Run subwave init now?`, then\n`Bring the stack up now?`) and the stack is on-air. `subwave setup`\nconnects Navidrome and your LLM, or do the same in the browser at\n`http:\u002F\u002Flocalhost:7700\u002Fonboarding`.\n\nNo clone, no Node on the host. `subwave status \u002F logs \u002F doctor \u002F update`\nwork from anywhere afterwards.\n\n## Quick start (no CLI, raw docker)\n\nIf you'd rather skip our binary on your host and stick to `docker compose`:\n\n```bash\nmkdir subwave && cd subwave\ncurl -O https:\u002F\u002Fraw.githubusercontent.com\u002Fperminder-klair\u002Fsubwave\u002Fmain\u002Fdocker-compose.yml\ncurl -O https:\u002F\u002Fraw.githubusercontent.com\u002Fperminder-klair\u002Fsubwave\u002Fmain\u002F.env.example\nmv .env.example .env\n# Edit .env: set ADMIN_USER, ADMIN_PASS, SITE_URL (three vars, that's it).\ndocker compose up -d\n# Then open https:\u002F\u002Fyour-host\u002Fonboarding. The web wizard collects Navidrome,\n# LLM, TTS, DJ persona, and offers to render jingles.\n```\n\nFunctionally identical: same images, same state layout, same persistence.\nThe CLI just saves you the curl-and-edit dance and gives you `subwave logs`,\n`subwave doctor`, etc. for the rest of the lifecycle.\n\n### Heavy TTS engines (optional)\n\nChatterbox (zero-shot voice cloning) and PocketTTS (fast multilingual) live in\na separate `subwave-tts-heavy` sidecar that adds ~5–6 GB of PyTorch and is\n**not** started by default. To enable:\n\n```bash\ndocker compose --profile tts-heavy up -d\n```\n\nThe controller is wired up to discover the sidecar automatically. Stop it\nagain with `docker compose --profile tts-heavy stop tts-heavy`; the rest of\nthe stack keeps running and Chatterbox\u002FPocketTTS personas silently fall back\nto Piper. The old `docker build --build-arg WITH_CHATTERBOX=1` path still\nworks if you already have a custom-built controller image — see\n`docker\u002FDockerfile.controller`.\n\n### Local dev (contributors)\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fperminder-klair\u002Fsubwave.git && cd subwave\n.\u002Fscripts\u002Fsetup.sh                                  # scaffolds a 3-var root .env + state\u002F\ndocker compose -f docker-compose.dev.yml up -d      # Broadcast (icecast2 + liquidsoap) + Controller\ncd web && npm install && npm run dev                # web UI on :7700, separate and hot-reloading\n# Then http:\u002F\u002Flocalhost:7700\u002Fonboarding to finish configuration.\n```\n\nDev compose bind-mounts `controller\u002Fsrc\u002F`, `radio.liq`, and `sounds\u002F` from the\nrepo. Controller runs under `tsx watch` so `src\u002F**` edits hot-reload inside\nthe container; `radio.liq` edits just need a `docker compose -f docker-compose.dev.yml restart broadcast`.\n\nThe standalone `subwave` CLI works inside the cloned repo too. `cd subwave &&\nsubwave start dev` does the right thing. The contributor convenience is `npm\nstart`, which `tsx`-runs the CLI source directly so unreleased changes are\nexercised. Same commands, same flags, no `npm install -g` needed.\n\nThe same CLI doubles as the console for running the station. Run `npm start`\nfor a status-aware menu; every menu action is also a one-shot subcommand,\nappended after `npm start --`:\n\n```bash\nnpm start                       # interactive operator console (status-aware menu)\nnpm start -- setup              # first-boot wizard: Navidrome, LLM, admin, env files\nnpm start -- status             # compose env, services, now-playing, recent events\nnpm start -- doctor             # full diagnostic sweep\nnpm start -- start dev          # docker compose up -d (dev or prod)\nnpm start -- restart broadcast  # plain restart (radio.liq is bind-mounted in dev)\nnpm start -- restart controller # rebuild + recreate (source is COPY-d at build)\nnpm start -- logs controller    # tail one service\nnpm start -- play               # SUB\u002FWAVE TUI — the terminal player\nnpm start -- listen             # open the web player in a browser\nnpm start -- admin              # open the admin console in a browser\nnpm start -- stop               # docker compose down (confirms first)\n```\n\n## Production deploy\n\nSingle Linux host, Cloudflare terminating TLS, Caddy routing to four internal\nservices. The [no-CLI quickstart above](#quick-start-no-cli-raw-docker) is\nthe canonical path: `curl` two files, fill in three vars, `docker compose\nup -d`, finish setup in the browser. See **[`DEPLOY.md`](DEPLOY.md)** for host\nprerequisites, Cloudflare setup, updates, and backup.\n\n**Bring your own reverse proxy.** If you already run Traefik, nginx, or your\nown Caddy in your homelab, swap the bundled-Caddy compose for the BYO variant:\n\n```bash\ndocker compose -f docker-compose.byo.yml up -d\n```\n\nThat exposes the web UI on `:7700`, the controller API on `:7701`, and the\nIcecast stream on `:7702` (all configurable). Point your proxy at those three.\n`docker\u002FCaddyfile` is a working reference for the route table you need to\nreplicate. Details in [`DEPLOY.md`](DEPLOY.md#bring-your-own-reverse-proxy).\n\n**Images on GHCR.** Tagged releases publish to `ghcr.io\u002Fperminder-klair\u002Fsubwave-{caddy,broadcast,controller,web}`.\nAll compose files pull `:latest` by default; pin a version with\n`SUBWAVE_VERSION=v1.2.3` in the root `.env`.\n\n## Repository layout\n\n```\ndocker-compose.yml      Production deploy with bundled Caddy (default)\ndocker-compose.byo.yml  Production deploy for hosts with their own reverse proxy\ndocker-compose.dev.yml  Local dev (broadcast + controller only; web runs separately)\ncontroller\u002F        Node.js controller, the AI DJ brain\n  src\u002Fllm\u002F         LLM layer (AI SDK): provider registry, prompts, tools\n  src\u002Fbroadcast\u002F   queue, session, DJ agent, scheduler, jingles\n  src\u002Fmusic\u002F       Subsonic client, pool picker, library tagging\n  src\u002Faudio\u002F       TTS engines: Piper, Kokoro, Chatterbox, PocketTTS, cloud\n  src\u002Froutes\u002F      HTTP API split by surface (public, request, onboarding, settings, …)\nliquidsoap\u002F        radio.liq, the Liquidsoap mixing pipeline\nweb\u002F               Next.js 15 web UI (player, landing, admin, setup)\ntui\u002F               Terminal player, the listener UI in your terminal\ndocker\u002F            Caddyfile, Dockerfiles, icecast.xml.template, supervisor entrypoint\nscripts\u002F           setup, jingle generation, update, health check\nmcp-subwave\u002F       MCP server that lets an agent request songs \u002F drive the DJ\ncli\u002F               Operator CLI (TS, run via tsx loader, no build step)\nbin\u002Fsubwave        Operator CLI entry: setup, status, doctor, lifecycle, play\n```\n\n## Notable details\n\n- **Controller code needs a rebuild, not a restart**, because its source is\n  `COPY`d at image build time. `radio.liq` is bind-mounted, so a Liquidsoap\n  restart is enough after editing it.\n- **The LLM provider is swappable at runtime** from the admin UI. Every model\n  call goes through the Vercel AI SDK.\n- **There is no `\u002Fskip` for listeners.** Track-end is the only natural\n  transition; operators have an admin-only skip endpoint.\n- Several areas (queue\u002Fplayback path, `radio.liq`, the crossfade, voice\n  ducking, the LLM layer) have **non-obvious constraints** that are easy to\n  regress. Read the relevant note in **[`CLAUDE.md`](CLAUDE.md)** before\n  touching them.\n\n## Documentation\n\n- **[`DEPLOY.md`](DEPLOY.md):** production deployment, updates, backup.\n- **[`CLAUDE.md`](CLAUDE.md):** deep architecture reference and the\n  non-obvious constraints behind each subsystem.\n- **[`CONTRIBUTING.md`](CONTRIBUTING.md):** how to contribute.\n- **[`SECURITY.md`](SECURITY.md):** reporting security issues.\n- **[`mcp-subwave\u002FREADME.md`](mcp-subwave\u002FREADME.md):** the MCP server.\n\n## License\n\n[MIT](LICENSE).\n","SUB\u002FWAVE 是一个个人互联网广播站，使用 Icecast 和 Liquidsoap 技术结合 AI DJ 自动化音乐播放和语音播报。其核心功能包括由AI DJ负责挑选歌曲并进行语音播报如天气、时间等信息，支持听众以自然语言请求播放特定风格或歌手的音乐。此外，项目还允许用户自定义音乐库，并且可以灵活更换LLM提供商以及选择多种TTS引擎。适合希望创建个性化在线广播服务、需要高度定制化内容管理及互动体验的场景使用。","2026-06-11 04:00:12","CREATED_QUERY"]