# 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 1. [Quick start](#quick-start) 2. [How to use the app](#how-to-use-the-app) - [The main page](#the-main-page) - [The five result cards](#the-five-result-cards) - [The settings page](#the-settings-page) - [Keyboard shortcuts](#keyboard-shortcuts) 3. [Features in depth](#features-in-depth) - [Surprise me / Go crazy (style buttons)](#surprise-me--go-crazy-style-buttons) - [Regenerate one section](#regenerate-one-section) - [Edit, then Revert](#edit-then-revert) - [Recent generations](#recent-generations) - [Download the ZIP](#download-the-zip) - [Cancel a long generation](#cancel-a-long-generation) 4. [Tips & tricks](#tips--tricks) 5. [Troubleshooting](#troubleshooting) 6. [For developers](#for-developers) --- ## Quick start You need **Node.js 18+** on your machine (or a server you can deploy to). ```sh # 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) ```sh # 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 . 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 ```sh 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`. ### Docker / Jannik-Cloud Reference files are in [`deploy/`](./deploy). See [`deploy/README.md`](./deploy/README.md) for the full deploy guide. --- ## 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-history` for recent generations, `melodymuse-theme` for 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: 1. The button shows a spinner. 2. The browser calls `POST /api/style/random` on the server. 3. The server calls the LLM with a small dedicated system prompt. 4. The returned one-line style description replaces the value in the **Music style** textarea. 5. 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 `context` so 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 start` or `node server.mjs`). - `LLM_ENDPOINT` and `LLM_API_KEY` are set in the server's env. - Open — 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/* ├── deploy/ # reference Dockerfile, compose, caddy, env script ├── 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 check - `POST /api/generate` — main generation - `POST /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: ```json { "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: ```json { "mode": "normal | crazy" } ``` Response: `{ "style": "…", "mode": "…" }`. `GET /api/health` — response: ```json { "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 `localStorage` keys the SPA writes: - `melodymuse-history` — last 6 generations - `melodymuse-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.