Files

484 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <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
```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 <kbd>⌘/Ctrl + Enter</kbd> 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 |
|---|---|
| <kbd>⌘/Ctrl + Enter</kbd> in the idea textarea | Generate |
| <kbd>Esc</kbd> 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`).
- <kbd>Esc</kbd> 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`:**
```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.