MelodyMuse
Generate every asset you need for a Suno AI song from a free-text idea — titles, lyrics, style prompt, video prompts (Abstract / Cinematic / Hybrid), and a YouTube description — then download everything as a single ZIP.
The app runs as a single Node.js process that serves both the built SPA and the API the SPA calls. Your LLM provider's API key lives on the server (env var), never in the browser.
[Browser] ──► [Node server :3000] ──► [LLM provider]
(no key) (holds the key)
Table of contents
Quick start
You need Node.js 18+ on your machine (or a server you can deploy to).
# 1. Clone
git clone https://git.orfel.de/Jannik/MelodyMuse.git
cd MelodyMuse
# 2. Install
npm install
# 3. Tell the server where your LLM lives and which key to use
export LLM_ENDPOINT="https://api.minimax.chat/v1"
export LLM_API_KEY="sk-…"
# Optional:
export LLM_MODEL="MiniMax-M3" # default
# 4. Run the dev server (two terminals — see below)
Dev workflow (two terminals)
# Terminal 1 — the API/proxy server (port 3000)
npm run dev:server
# Terminal 2 — the Vite dev server (port 5173, hot reload)
npm run dev
Open http://localhost:5173. The Vite dev server proxies /api/* to the
Node server on :3000 (see vite.config.ts), so the SPA always talks to
the same origin.
Production
npm run build # writes the static SPA to dist/
npm start # node server.mjs, serves dist/ + /api/*
Then put a reverse proxy (Caddy, nginx, …) in front of localhost:3000.
Deployment
This repo contains only the app — the Dockerfile, docker-compose,
Caddy fragment, and .env generation script for the Jannik-Cloud
service live alongside the rest of the Jannik-Cloud services. To
deploy, copy them into a new services/melodymuse/ directory there.
How to use the app
The main page
Two columns on desktop, stacked on mobile.
Left — the input panel
- Music idea — describe what you want. The more concrete, the better the result. Press ⌘/Ctrl + Enter to generate.
- Try an example — fills the input with a random starter idea.
- Options — click to expand:
- Language — lyrics language. Choose "Other" to type a custom one.
- Music style — an optional style description. Two buttons:
- Surprise me — generates a normal, Suno-friendly style.
- Go crazy — generates an unusual genre mashup.
- Mood (optional) — an emotional hint, e.g. "melancholic".
- Vocals — 🎤 Vocals or 🎹 Instrumental.
- Generate Song Assets — main button. Spinner + elapsed time during generation. Cancel generation button appears next to it.
- Recent generations (below the form) — last 6 saved generations.
Right — the results panel
Shows the five result cards once a generation completes. Each card is editable and individually regenerable. The sticky results header has a "Regenerate All" button. Once you've picked a title, the Download ZIP bar appears at the bottom of the viewport.
The five result cards
| Card | What it does | Editable? | Regenerate? |
|---|---|---|---|
| 🏷️ Song Titles | 3 short title suggestions. Click to select. | select only | yes |
| 📝 Lyrics | Full lyrics with Suno section tags. | yes | yes |
| 🎵 Style | Style prompt and negative style. | yes | yes (both at once) |
| 🎬 Video Prompts | Abstract / Cinematic / Hybrid tabs. | yes | yes |
| 📺 YouTube Description | Full description with embedded lyrics + hashtags. | yes | yes |
Each editable card has a Revert button (top-right) that appears the moment you change the value. Click it to restore the last generated version.
The settings page
Click Settings (under the generate button) or navigate to /settings.
- Server status — green check if the API server is reachable and configured. Click Re-check to refresh. Shows the model name and endpoint so you know which LLM the server is using.
- Local data — see what's stored in your browser (
melodymuse-historyfor recent generations,melodymuse-themefor the theme). You can clear individual buckets or everything. - Theme — toggle the moon/sun icon in the header.
The API key is not here — it lives on the server in the LLM_API_KEY
environment variable.
Keyboard shortcuts
| Key | Action |
|---|---|
| ⌘/Ctrl + Enter in the idea textarea | Generate |
| Esc while a generation is in flight | Cancel it |
Features in depth
Surprise me / Go crazy (style buttons)
Both buttons are in the Options → Music style row.
| Button | What the model is told |
|---|---|
| ✨ Surprise me | "Generate ONE short, cohesive, production-ready Suno style description. Comma-separated keywords. Max 25 words." |
| 🔥 Go crazy | "Generate ONE short style description that DELIBERATELY combines genres/eras/instruments that don't normally mix. Still parseable by Suno. Max 25 words." |
When you click a button:
- The button shows a spinner.
- The browser calls
POST /api/style/randomon the server. - The server calls the LLM with a small dedicated system prompt.
- The returned one-line style description replaces the value in the Music style textarea.
- The spinner stops.
You can keep clicking — each click fetches a new style. The field is overwritten, never appended. Both buttons are disabled while a request is in flight, so you can't double-fire.
If the field has a value when you click Generate Song Assets, that value
is sent to the model as a style_hint and used as the basis for the full
style field in the output.
Regenerate one section
Every card has a Regenerate button in its header.
- Click it to ask the model to rewrite that section only.
- The rest of the current song is passed as
contextso the new section stays consistent with the existing lyrics, mood, and vocabulary. - Only that card's spinner is active while it runs; the other cards stay fully usable.
Edit, then Revert
Every editable card compares its current value against the last generated value. If they differ, a Revert button appears in the card header. Click it to snap back to the last generated version.
This is great for "I'll tweak this one line and see if I like it better" without losing the original.
Recent generations
The last 500 successful generations are saved on the server. Click any entry to restore both the input form values AND the full generated assets — handy for comparing two generations of the same idea.
Individual entries have an ✕ button to remove them. There's a Clear history button at the bottom of the history panel.
Download the ZIP
Once you've picked a title (the first one is auto-selected on generation),
a sticky bar appears at the bottom of the viewport showing
📁 [Title].zip and a Download ZIP button. Click it to download a ZIP
with four files:
| File | Contents |
|---|---|
Style.txt |
STYLE PROMPT: … + NEGATIVE STYLE: … |
Text.txt |
The full lyrics |
Videodescription.txt |
The YouTube description |
Videoprompt.txt |
All three video prompts in order (Abstract, Cinematic, Hybrid), each with the negative line and tool recommendation |
The filename is the selected title, sanitized: spaces become _, anything
that's not a letter, digit, underscore, or dash is removed. If the title
sanitizes to an empty string, the file is named song.zip.
Cancel a long generation
While a generation is running:
- A Cancel generation button appears below the main generate button.
- A Cancel button appears in the sticky results header (top right of the right panel).
- The Generate button shows a live elapsed-time counter
(e.g.
Generating… 8s). - Esc also works.
Cancellation aborts the in-flight HTTP request. The server also aborts its upstream request to the LLM, so no tokens are wasted.
Tips & tricks
- Be specific in the idea. "Melancholic lo-fi house beat for a rainy Sunday morning" produces better results than "make a song". Mood, tempo, era, setting, instrumentation — all help.
- Use Surprise me to break out of a rut. Click it 3–4 times; one of the styles will spark a new direction.
- Use Go crazy for instant novelty. "Baroque chamber orchestra meets dubstep" might be exactly the brief you needed.
- Style hint + surprise = best of both. Click Surprise me, then tweak the words slightly before generating. The model uses your edit as the basis for the full 120-word style description.
- Revert before regenerating. If you edited a card and the edit isn't quite right, click Revert to restore the last generated version, then click Regenerate to get a fresh alternative.
- History is your scratch pad. If you generate 6 variations, none of them is lost — pick the best from history.
Troubleshooting
"Server offline" / "Server unreachable"
The SPA can't reach the Node server. Check:
- The server is running (
npm startornode server.mjs). LLM_ENDPOINTandLLM_API_KEYare set in the server's env.- Open http://localhost:3000/api/health — it should return
{ "ok": true, ... }.
"Provider returned 401 / 403"
The LLM_API_KEY is wrong, expired, or doesn't have access to the model.
Update the env var on the server and restart.
"Provider returned 404"
Either the LLM_ENDPOINT is wrong, the path /chat/completions doesn't
exist at that URL, or the model name (LLM_MODEL) doesn't exist for that
provider.
"Could not reach ${url}" with fetch failed
The server can't reach the LLM provider. If you're running the server in a container, check that it has network access to the provider. Some providers block known cloud-IP ranges — check the provider's allow-list.
The model returns prose instead of JSON
The system prompt asks for JSON only. If a model ignores that, the
server's extractJson tries three increasingly lenient fallbacks. If all
fail you'll see a clear error toast — re-try the generation, or try a
different model.
"Storage quota exceeded" toast
Your localStorage is full (a typical cap is 5–10 MB). Open Settings →
Local data → Clear all local data, or just clear recent generations.
I can't get a clean style out of "Go crazy"
The prompt is intentionally permissive — the model is told to "deliberately break conventions". If you get a style that Suno rejects, just click the button again.
For developers
Project layout
MelodyMuse/
├── src/ # React SPA
│ ├── App.tsx
│ ├── main.tsx
│ ├── index.css # Tailwind + design tokens
│ ├── components/ # UI primitives
│ │ ├── cards/ # The 5 result cards
│ │ ├── CopyButton.tsx
│ │ ├── EqualizerIcon.tsx
│ │ ├── Footer.tsx
│ │ ├── HistoryPanel.tsx
│ │ ├── InputPanel.tsx
│ │ ├── ResultCard.tsx
│ │ ├── ResultsPanel.tsx
│ │ ├── SkeletonCard.tsx
│ │ ├── StickyZipBar.tsx
│ │ └── ThemeToggle.tsx
│ ├── pages/
│ │ ├── HomePage.tsx
│ │ └── SettingsPage.tsx
│ └── lib/
│ ├── history.ts # recent generations persistence
│ ├── llm.ts # browser → server client (thin)
│ ├── theme.ts # dark/light + localStorage
│ ├── toast.tsx # toast context
│ ├── types.ts # shared TS types
│ ├── useAutoHeight.ts # textarea auto-grow hook
│ ├── useElapsed.ts # elapsed-seconds hook
│ └── zip.ts # JSZip layout
├── server/
│ └── prompts.mjs # system + user prompt construction
├── server.mjs # ★ the runtime — serves SPA + /api/*
├── public/favicon.svg
├── index.html
├── tailwind.config.js
├── vite.config.ts
├── tsconfig*.json
├── package.json
└── README.md
Architecture
The browser only ever talks to one server: the Node process in
server.mjs. That server:
- Serves the built SPA from
dist/ - Exposes three JSON endpoints:
GET /api/health— health checkPOST /api/generate— main generationPOST /api/style/random— single-line Suno style
- Holds the LLM credentials in
process.env
The browser-side code is a thin wrapper around fetch. The system
prompts, JSON extraction, and shape validation all live on the server in
server/prompts.mjs and server.mjs. The browser never sees the
provider, the key, or the prompts.
Scripts
| Command | What it does |
|---|---|
npm run dev |
Vite dev server (port 5173) with HMR. Proxies /api/* to :3000. |
npm run dev:server |
The Node server in dev (no build step). |
npm run build |
Type-check + build the SPA to dist/. |
npm start |
Run the Node server (uses the existing dist/). |
npm run preview |
Vite preview server (no HMR). |
npm run lint |
Type-check only. |
Environment variables (server)
| Var | Required | Default | Notes |
|---|---|---|---|
LLM_ENDPOINT |
✅ | — | e.g. https://api.minimax.chat/v1 |
LLM_API_KEY |
✅ | — | Provider secret |
LLM_MODEL |
MiniMax-M3 |
||
PORT |
3000 |
||
CORS_ORIGIN |
* |
Lock this down in production | |
DATA_DIR |
data |
Directory for persistent storage (e.g. history.json) |
Docker and History Persistence
The application saves recent generations (history) to a file named history.json inside the directory specified by the DATA_DIR environment variable (defaults to data in the project root).
If you are running the application using Docker, any files written inside the container will be lost when the container is rebuilt or restarted. To ensure your history survives a Docker rebuild, you must set up a Docker volume mapped to the DATA_DIR.
Example using docker run:
docker run -d \
-p 3000:3000 \
-e LLM_ENDPOINT="https://api.minimax.chat/v1" \
-e LLM_API_KEY="sk-..." \
-e DATA_DIR="/app/data" \
-v melodymuse-data:/app/data \
melodymuse-image
Example using docker-compose.yml:
services:
melodymuse:
image: melodymuse-image
ports:
- "3000:3000"
environment:
- LLM_ENDPOINT=https://api.minimax.chat/v1
- LLM_API_KEY=sk-...
- DATA_DIR=/app/data
volumes:
- melodymuse-data:/app/data
volumes:
melodymuse-data:
This ensures the history.json file is securely stored on your host machine and persists across rebuilds.
Environment variables (Vite, dev only)
| Var | Default | Notes |
|---|---|---|
VITE_API_BASE_URL |
empty | Override the API base URL. Leave empty in dev (Vite's proxy handles it) and in same-origin production deployments. |
API contracts
POST /api/generate — body:
{
"input": "user's music idea (required)",
"language": "English",
"mood": "optional",
"style_hint": "optional — short style description",
"vocals": "vocals | instrumental",
"section": "all | lyrics | style | titles | video_prompts | youtube_description",
"context": { /* full or partial SongAssets, used for partial regen */ }
}
POST /api/style/random — body:
{ "mode": "normal | crazy" }
Response: { "style": "…", "mode": "…" }.
GET /api/health — response:
{ "ok": true, "llm_configured": true, "model": "MiniMax-M3", "endpoint": "https://…" }
Security notes
- The API key is held by the Node process via
process.env. It's never sent to the browser in any response, not even in the health check. - The
localStoragekeys the SPA writes:melodymuse-draft— current unsaved input draftmelodymuse-theme—'dark'or'light'melodymuse-config— reserved, currently unused (kept for future client-side server-URL override)
- You can wipe any of them from Settings → Local data, or programmatically with the browser's DevTools.
License
Personal use. Do whatever you want with it.