2026-06-03 01:25:22 +02:00
2026-07-04 22:51:07 +02:00
2026-06-03 01:25:22 +02:00

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
  2. How to use the app
  3. Features in depth
  4. Tips & tricks
  5. Troubleshooting
  6. For developers

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-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 34 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 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 510 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:

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 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.

S
Description
No description provided
Readme
593 KiB
Languages
TypeScript 70.9%
JavaScript 23.3%
CSS 5.6%
HTML 0.2%