A Node.js daemon that runs a shared MP3 broadcast with songs generated by a remote ACE-Step server and lyrics, titles, and band names from an OpenAI-compatible chat-completions endpoint. All listeners receive one continuous stream (Icecast/SHOUTcast-style, with optional ICY metadata).
- Node.js ≥ 20
- Install:
pnpm install - Build:
pnpm run build(compiles TypeScript daemon and tests) - Run:
pnpm start(runsdist/index.js)
On first run, config.json is created in the working directory with sensible defaults. Edit it to point to your ACE-Step and text-generation endpoints, then restart.
Edit config.json (created with defaults on first run). The example below is illustrative — the generated default file has empty bands/themes and no streamListen; optional fields (per-theme audioDuration, streamListen) are shown here for reference:
{
"stationName": "aiCast",
"listen": { "host": "127.0.0.1", "port": 8008 },
"dataDir": "./data",
"bands": [
{
"prompt": "dark pop",
"language": "en",
"bandName": "",
"comment": ""
}
],
"themes": [
{ "band": "<bandKey>", "prompt": "a song about rain", "enabled": true, "audioDuration": 120 },
{ "band": "<bandKey>", "prompt": "", "enabled": true }
],
"audio": {
"url": "http://localhost:8001",
"apiKey": "",
"api": "acestep"
},
"text": {
"url": "http://localhost:8080/api/v1",
"apiKey": "",
"model": ""
},
"generation": {
"audioDuration": 180,
"inferenceSteps": 8,
"quality": 2,
"lookahead": 1
},
"streamListen": { "host": "0.0.0.0", "port": 9000 }
}- stationName: Broadcast name (displayed in listeners and ICY metadata)
- listen.host, listen.port: Interface and port for the HTTP server
- dataDir: Directory for library MP3s and state file
- bands: Style definitions. Each entry has:
prompt: music description (e.g. "dark pop", "ambient")language: exactly two ISO 639-1 letters (e.g. "en", "it")bandName: auto-filled by the server via LLM (ignored on PUT)comment: optional note
- themes: The ordered play queue. Each entry has:
band: the owning band's key (server-derived, from thebandKeyfield in GET /api/config)prompt: lyric subject; empty or omitted means instrumentalenabled: toggle the row in/out of rotationaudioDuration(optional): override the global duration for this row (10–600 seconds); absent uses global
- audio.{url,apiKey,api}: ACE-Step endpoint (
acestep= native REST,acestep.cpp= GGML variant) - text.{url,apiKey,model}: OpenAI-compatible endpoint for band names and lyrics
- generation.audioDuration: default song length in seconds (per-theme override available)
- generation.inferenceSteps: inference steps for audio generation
- generation.quality (optional): MP3 quality (0=best … 8=worst; empty/absent = backend default; only applied on
acestep.cppbackend) - generation.lookahead: prefetch queue size
- streamListen (optional):
{ host, port }— dedicated listener for/stream. When set,/streamis served only on this listener and omitted from the admin listener. Must differ fromlisten. Requires restart to change.
| Command | Action |
|---|---|
pnpm run build |
Compile daemon and tests to dist/ |
pnpm run tests |
Run AVA test suite |
pnpm start |
Run the daemon |
Open http://localhost:8008 in a browser. The panel lets you:
- Edit station name, listen address, and player settings
- Manage bands and the ordered theme queue (add/remove bands, edit themes, reorder queue rows)
- Configure ACE-Step and text-generation endpoints
- Set audio duration, inference steps, quality, and optional dedicated stream listener
- Watch live status: listener count, current song, generation queue
- Copy stream URL
Built with myopie.js (reactive components) and jTDAL (template engine), both loaded from CDN.
| Method | Path | Description |
|---|---|---|
GET |
/stream |
MP3 broadcast (audio/mpeg, Icecast/SHOUTcast-style) |
GET |
/ |
Control panel HTML |
GET |
/public/* |
Static assets (CSS, JS) |
GET |
/api/config |
Full config with derived keys (JSON) |
PUT |
/api/bands |
Save bands array; server owns band names |
PUT |
/api/themes |
Save themes array (ordered play queue) |
PUT |
/api/settings |
Save scalar/settings sections (no bands/themes) |
GET |
/api/status |
Live status: listener count, current song, generation queue, play counts |
POST |
/api/reset |
Reset play counters ({}, { band }, or { band, theme }) |
- src/ — TypeScript sources (strict, ESM,
.jsextensions in imports) - dist/ — compiled daemon and build artifacts (committed)
- public/ —
panel.js,aicast.css - tests/ — AVA test suite
- data/ — library MP3s and
state.json - build.mjs — build orchestrator
Generated MP3s live in data/library/ with filenames <styleKey>_<lyricsKey>_NNNNN.mp3. data/state.json persists per-series play counters and the playback cursor (playCursor); band names are stored in config.json. Each generated track also carries its ACE-Step seed in the ID3v2 comment as seed: <value> — the daemon picks a random seed per track and sends it to the backend, so a track can be reproduced.
Two ACE-Step backends available behind one AudioClient interface:
- acestep: native REST API
- acestep.cpp: GGML-based variant
Set audio.api in config to choose.
Audio is always requested and stored as MP3 directly from the backend; lame/WAV transcoding has been removed.
See AGENTS.md and the source code for the authoritative design details.