# 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`. ### 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-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 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 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/* ├── 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 | | `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`:** ```sh 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`:** ```yaml 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: ```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-draft` — current unsaved input draft - `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.