A fully local, offline AI radio app. Pick a genre, mood, vocal language, and describe what you're doing — the app generates and plays an endless stream of original AI-composed songs with no cloud APIs required.
Available as a web app (React + Vite) and a mobile app (Expo / React Native) for iOS and Android.
- Mac with Apple Silicon (M1/M2/M3/M4/M5)
- macOS 14+
- 16 GB+ unified memory (24 GB+ recommended for development, 64 GB for production)
- 50 GB+ free SSD space
./scripts/setup.shThis installs Homebrew tools, Ollama, the LLM model, clones ACE-Step 1.5, installs all dependencies, and installs cloudflared for remote access.
echo 'export PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0' >> ~/.zshrc
echo 'export PYTORCH_ENABLE_MPS_FALLBACK=1' >> ~/.zshrc
source ~/.zshrcDevelopment (hot-reload):
./scripts/start.shProduction (compiled bundle, no reload):
./scripts/start_prod.shOpen http://localhost:5173 in your browser.
When cloudflared is installed, a public URL is printed in the startup banner — share it to access the app from any device.
- Select a genre (36 options) and optional mood keywords (60 keywords across 4 categories)
- Choose a vocal language (11 languages) or instrumental mode
- Optionally describe what you're doing now in free text
- Optionally tune advanced ACE-Step parameters (time signature, inference steps, model variant, CoT flags)
- Click Start Radio
- A local LLM (Ollama + Qwen3.5:4b) generates a dimension-based song prompt (style, instruments, mood, vocal style, production)
- ACE-Step 1.5 generates a full MP3 with semantic audio codes for melodic structure
- The song plays in your browser with a live activity log showing generation progress
- The next song is pre-generated while the current one plays — the frontend pre-fetches audio bytes into memory for seamless, zero-latency transitions
Multiple browsers can connect to the same session. The first local-network connection becomes the controller — they pick genres, start/stop the radio, save tracks, and see connected listeners. Everyone else joins as a viewer with a read-only player.
Remote visitors connecting via the Cloudflare tunnel always join as viewers regardless of order.
If the controller disconnects, the next local viewer is automatically promoted.
Viewers can request the DJ slot via the Be the DJ button. When granted:
- A DJ panel opens where the viewer enters their name and configures genre, mood, and language
- Their selection becomes the next track's generation parameters
- A cooldown timer (configurable, default 30 min) prevents rapid DJ switching
- The active DJ's name is shown in the player ("PRESENTED BY [NAME]")
The controller can navigate back to the genre selector at any time without stopping the current track. The new settings take effect from the next generated track onward.
The controller can save the currently playing track to disk — both the MP3 and a JSON metadata file (title, genre, BPM, key, seed, lyrics, tags) are written to saved_tracks/. Remote viewers cannot trigger saves.
Any connected listener (controller or viewer) can react to the currently playing track with a thumb up or thumb down. Reactions use toggle semantics — pressing the same button again removes the vote; pressing the opposite side switches. Reaction counts are broadcast to all listeners in real time and persisted to disk.
English, Español, Français, Deutsch, Italiano, 中文, Ελληνικά, Suomi, Svenska, 日本語, 한국어, and a No Vocal (instrumental) mode.
The controller can configure ACE-Step parameters before starting:
| Option | Default | Range |
|---|---|---|
| Time Signature | Auto | 2/4, 3/4, 4/4, 6/8 |
| Inference Steps | 8 | 4–100 (more = higher quality, slower) |
| DiT Model Variant | turbo | turbo, turbo-shift1, turbo-shift3, turbo-continuous |
| ACE-Step CoT Flags | Thinking ON, CoT Caption/Metas OFF, CoT Language ON | per-flag toggles |
| DJ Cooldown | 30 min | 1–120 min |
See the ACE-Step 1.5 Tutorial for details on what each parameter does.
start.sh supports two tunnel modes:
Named tunnel (production): If ~/.cloudflared/config.yml is configured, the app is available at a fixed domain (e.g., https://radio.scrambler-lab.com). See docs/cloudflare-named-tunnel-setup.md for one-time setup.
Quick tunnel (dev fallback): If no named tunnel is configured, a random *.trycloudflare.com URL is generated on each startup.
Both modes proxy all traffic including WebSockets. Viewers joining via the tunnel automatically get the read-only listener experience.
The mobile/ directory contains an Expo / React Native app for iOS and Android. The mobile app is always a viewer — it connects to the same backend WebSocket and plays the radio stream, but cannot control the session (no genre selector, no save track). It is designed for listening on the go while the desktop session drives generation.
- Xcode (iOS) or Android Studio (Android)
- Node.js + npm
- Expo CLI:
npm install -g expo-cli
cd mobile
npm install
npx expo prebuild # generates ios/ and android/ from app.json
npx expo run:ios --device # or: eas build --platform iosSee docs/ios-simulator-guide.md for iOS setup and docs/android-setup-guide.md for Android setup.
The mobile app uses expo-audio with a silence bridge pattern to keep the iOS audio session alive during track transitions (AI generation can take 30–120 s). Background audio requires a production build — it does not work in Expo Go.
After making changes to app.json (plugin config, permissions), run npx expo prebuild --clean before rebuilding.
See docs/ios-background-audio-investigation.md for the full background audio analysis.
| Service | Port | Description |
|---|---|---|
| Frontend | 5173 | React + Vite (dev HMR or compiled preview, proxies /api and /ws) |
| Backend | 5555 | FastAPI (REST + WebSocket) |
| ACE-Step API | 8001 | Music generation (MLX / Apple Silicon) |
| Ollama | 11434 | LLM inference |
| Cloudflare Tunnel | — | Exposes port 5173 publicly (optional) |
Mobile connects directly to the backend WebSocket (wss://radio.scrambler-lab.com/ws in production, ws://localhost:5555/ws in dev).
See BUILD_SPEC.md for the full technical specification.
The app always uses qwen3.5:4b (~2.5 GB) for song prompt generation, generating 5 dimension fields (style, instruments, mood, vocal style, production) that are concatenated into a rich ACE-Step caption.
Audio duration is selected automatically at startup based on unified memory:
| Memory | Duration | Rationale |
|---|---|---|
| ≤ 32 GB | 30 s | Fast iteration on dev machines |
| 33–47 GB | 60 s | Safe within MLX VAE Metal buffer limits |
| ≥ 48 GB | 60 s → 120 s → 180 s | Progressive ramp — first track starts quickly, subsequent tracks get longer |
See docs/acestep-memory-vs-duration.md for the full memory vs. duration analysis.
All services write logs to /tmp/:
tail -f /tmp/generative-radio-backend.log # FastAPI backend
tail -f /tmp/generative-radio-acestep.log # ACE-Step API
tail -f /tmp/generative-radio-frontend.log # Vite dev server
tail -f /tmp/generative-radio-cloudflared.log # Cloudflare tunnelBackend log format: HH:MM:SS [LEVEL] module: [component] message
Frontend logs are in the browser DevTools console with [WS], [Radio], [Audio], and [GenreSelector] prefixes.
# Override ACE-Step location
ACESTEP_PATH=/path/to/ACE-Step-1.5 ./scripts/start.sh
# Run backend directly with custom log level
cd backend
uvicorn main:app --port 5555 --log-level debug
# Run frontend (dev, hot-reload)
cd frontend
npm run dev
# Run frontend (production preview of compiled bundle)
cd frontend
npm run build && npm run previewPress Ctrl+C in the terminal running start.sh — the backend, frontend, and Cloudflare tunnel are all shut down cleanly.
ACE-Step is intentionally left running because it takes several minutes to warm up. To stop it manually, use the PID printed in the startup banner:
kill <ACESTEP_PID>