The Dockerfile, docker-compose snippet, Caddy fragment, and generate-env.sh now live alongside the rest of the Jannik-Cloud services — that's the one place that already knows how to manage them. The MelodyMuse repo is now app-only: source, build config, the Node server, and docs. The README's 'Docker / Jannik-Cloud' subsection is replaced with a short note pointing the reader at the Jannik-Cloud service directory and the project-layout section no longer lists deploy/.
16 KiB
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 6 successful generations are saved in localStorage under
melodymuse-history. 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 all 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 |
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-history— last 6 generationsmelodymuse-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.