---
name: suno-music-generation
description: Generate music via Suno API (kie.ai VEO3 proxy) with callback handling. VPS callback as systemd service, auto-download MP3s. Covers video-to-music sync using energy analysis.
---

# Suno Music Generation via VPS Proxy

Generate AI music through the VPS proxy (kie.ai Suno API) and auto-download via callback.

## Architecture

```
Local Machine (Mac/Linux)    VPS (Contabo)
┌─────────────────┐         ┌──────────────────────────┐
│ Video Factory   │   SSH   │ veo3-proxy.py (:18999)   │
│ ffmpeg mixing   │◄───────►│   → api.kie.ai (Suno)    │
│ edit_montage.py │         │                          │
│                 │         │ suno_callback_v2.py      │
│                 │         │   (systemd, :18998)      │
│                 │         │   ← callback from Suno   │
└─────────────────┘         └──────────────────────────┘
                                  │ nginx reverse proxy
                                  │ https://n8n.DOMAIN/suno-callback
                                  │ → :18998
```

## ⚠️ CRITICAL SUNO RESPONSE GOTCHAS (found via trial & error)

1. **`audio_url` is ALWAYS empty string `""`** in the callback JSON. The real audio URL is in `source_stream_audio_url` or `stream_audio_url`. The handler MUST check all fields.

2. **Callback JSON path**: The structure is nested: `body → data → data[]`
   ```json
   {"body": {"code": 200, "data": {"data": [{"id": "...", "title": "...", "source_stream_audio_url": "https://..."}]}}}
   ```
   So the items are at `data["body"]["data"]["data"]`.

3. **Old model `ai-music-api/generate` now returns 422**. Valid models: `V3_5`, `V4`, `V4_5`, `V4_5PLUS`, `V4_5ALL`, `V5`, `V5_5`. The cheapest for testing is `V3_5`.

4. **`customMode` is required** when using `customPrompt`. Without it → 422 error.

5. **Artist names are BLOCKED**. Suno's API returns 400 if you reference specific artists. Never use "Rammstein", "Metallica", etc. in `style` or `prompt`. Instead describe the sound. Example error: *"Your tags contain artist name rammstein - we don't reference specific artists"*.

6. **Generations with empty `callBackUrl` consume credits but are IRRETRIEVABLE**. The API is callback-only — there are NO status/polling endpoints. All `/api/v1/suno/*` return 404.

7. **Russian/Cyrillic in filenames breaks SCP**. Always `cp` to ASCII names before copying.

8. **Download speed from Suno pipes** is slow (200-750 KB/s for MP3). Set timeout ≥ 120s.

## Step 1: Callback Handler (systemd on VPS)

### Script: `/root/suno_callback_v2.py`

Must check `source_stream_audio_url` (not `audio_url` which is always empty):

```python
import json, os, urllib.request, time
from http.server import HTTPServer, BaseHTTPRequestHandler

OUTPUT_DIR = "/root/suno_music"
PORT = 18998

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get('Content-Length', 0)))
        data = json.loads(body)
        # Suno wraps in body→data→data[]
        payload = data.get("body", data).get("data", data)
        items = payload.get("data", [])
        if isinstance(items, dict): items = [items]
        
        for item in items:
            if not isinstance(item, dict): continue
            # KEY FIX: check source_stream_audio_url FIRST - audio_url is always ""
            audio_url = (item.get("source_stream_audio_url") or 
                        item.get("stream_audio_url") or
                        item.get("audio_url") or 
                        item.get("audioUrl") or "")
            if not audio_url:
                continue
            title = "".join(c if c.isalnum() or c in ' -_' else '_' for c in item.get("title","track"))
            fp = os.path.join(OUTPUT_DIR, f"{title}_{int(time.time())}.mp3")
            try:
                req = urllib.request.Request(audio_url, headers={"User-Agent": "Mozilla/5.0"})
                audio_data = urllib.request.urlopen(req, timeout=300).read()
                with open(fp, 'wb') as f: f.write(audio_data)
                print(f"[SUNO] OK {fp} ({len(audio_data)} bytes)")
            except Exception as e:
                print(f"[SUNO] DL fail: {e}")
                # Save metadata for retry
                meta = os.path.join(OUTPUT_DIR, f"pending_{int(time.time())}.json")
                with open(meta, 'w') as f: json.dump({"url": audio_url, "item": item}, f)
        self.send_response(200); self.end_headers(); self.wfile.write(b'{"status":"ok"}')
    def do_GET(self):
        files = sorted(os.listdir(OUTPUT_DIR))[-20:] if os.path.exists(OUTPUT_DIR) else []
        self.send_response(200); self.send_header("Content-Type","application/json")
        self.end_headers(); self.wfile.write(json.dumps({"status":"running","files":files}).encode())
    def log_message(self, *a): pass

os.makedirs(OUTPUT_DIR, exist_ok=True)
HTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
```

### systemd unit: `/etc/systemd/system/suno-callback.service`
```
[Unit]
Description=Suno Music Callback Handler
After=network.target
[Service]
ExecStart=/usr/bin/python3 /root/suno_callback_v2.py
WorkingDirectory=/root/suno_music
Restart=always
User=root
[Install]
WantedBy=multi-user.target
```
Start: `systemctl daemon-reload && systemctl enable --now suno-callback`

## Step 2: Generate Music

### API via VPS proxy (:18999)

### Valid models (found via trial & error)
The old `model: "ai-music-api/generate"` no longer works (422 error).

Valid models: `V3_5` (cheapest), `V4`, `V4_5`, `V4_5PLUS`, `V4_5ALL`, `V5`, `V5_5`.

Use `V3_5` for testing — it's the most affordable.

```bash
curl -X POST 'http://localhost:18999/api/v1/generate' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "V4",
    "customMode": true,
    "prompt": "Epic Russian heavy metal ballad, emotional male vocals in Russian, powerful clean vocals, acoustic then electric",
    "instrumental": false,
    "style": "Russian Rock, Power Ballad, Heavy Metal",
    "customPrompt": "[Intro] Нас мотало по волнам / [Verse] Шторм и штиль... / [Chorus] Лучшего места чем дом не найти...",
    "callBackUrl": "https://YOUR_PUBLIC_DOMAIN/suno-callback"
  }'
```

### Required fields
- `model` — one of V3_5, V4, V4_5, V5
- `customMode` — boolean (true for custom prompt)
- `prompt` — description (also used as lyrics prompt if no customPrompt)
- `callBackUrl` — **REQUIRED**. Must be publicly reachable. API is callback-only, no polling.
- `style` — comma-separated genre tags (NO artist names! Suno blocks them)
- `customPrompt` — lyrics with [Verse]/[Chorus]/[Bridge] markers
- `instrumental` — boolean

### ⚠️ Critical: Artist names are BLOCKED
Don't put "Rammstein" anywhere. Describe the sound instead:
- ❌ `style: "Rammstein, Industrial Metal"` → 400 error
- ✅ `style: "Industrial Metal, Neue Deutsche Härte, German industrial"`

## Step 3: Download & Mix (Mac)

After callback arrives, `*.mp3` files appear in `/root/suno_music/`.

```bash
# Rename to avoid Russian/Special chars in SCP
ssh VPS "cp '/root/suno_music/Имя файла.mp3' /root/suno_music/track_v1.mp3"
scp VPS:/root/suno_music/track_v1.mp3 /path/to/Music/

# Mix with video
ffmpeg -y -i video.mp4 -i track.mp3 -t 60 \
  -filter_complex "[1:a]atrim=0:60,volume=0.35[music];[0:a]volume=0.10[amb];[music][amb]amix=inputs=2:duration=first[audio]" \
  -map 0:v -map "[audio]" -c:v copy -c:a aac output.mp4
```

## Step 4: Sync Music Drops to Video Moments

### Find intense video moments
```bash
ffmpeg -i video.mp4 -filter:v "select='gt(scene,0.04)',showinfo" -f null - 2>&1 | grep "pts_time"
```

### Find loudest audio sections
```python
import numpy as np
# Convert to raw PCM 16-bit mono 22050Hz
samples = np.fromfile('/tmp/audio.raw', dtype=np.int16)
fs = 22050
energy = np.array([np.sqrt(np.mean(samples[i*fs:(i+1)*fs].astype(float)**2))
                   for i in range(len(samples)//fs)])
# Top 10 peaks
peak_seconds = np.argsort(energy)[-10:]
```

## Pitfalls

1. **`audio_url` is empty string** in callback JSON. Real URL is always in `source_stream_audio_url` or `stream_audio_url`. Handler must check all fields.

2. **Callback JSON path**: `body` → `data` → `data[]` (array of items), each with `id`, `title`, `source_stream_audio_url`.

3. **No status/polling endpoint**. ALL `/api/v1/suno/*` endpoints return 404. Callback-only.

4. **Generations with empty callBackUrl are LOST** — credits spent, no way to retrieve.

5. **nohup/& blocked** by Hermes terminal. Use systemd for server processes.

6. **Download is SLOW** (~200-750 KB/s from Suno pipes). Set curl timeout ≥ 120s.

7. **Russian/Cyrillic filenames** break SCP. Always `cp` to ASCII name first.

8. **SSL/HTTPS**: Callback URL must be HTTPS. Use existing nginx on VPS with reverse proxy to :18998.

9. **`ai-music-api/generate` model is dead**. Use V3_5/V4/V5 instead.

10. **Callback server MUST be running before generating**. Check first: `systemctl status suno-callback`. If server was down during generation, the callback never arrives and credits are lost. Pre-flight check: `curl -s http://localhost:18998/` should return JSON `{\"status\":\"running\",...}`.

11. **V3_5 confirmed working** for cheap tests (tested 2026-05-29). It generates Russian vocals with proper `customPrompt` containing [Verse]/[Chorus] markers. Tracks confirm: "Трещины Штормов" (heavy metal), "Бумажный флюгер" (ballad), "Соль на ладонях" (emotional rock). All had Russian titles and Russian vocals.

12. **Confirmed working callback URL**: `https://n8n.valentinnews.sailor.bar/suno-callback` — nginx reverse proxy → VPS localhost:18998 → systemd service runs `/root/suno_callback_v2.py`.

13. **User preference**: Russian vocals are REQUIRED. If Suno generates English/mixed, reject and regenerate with different prompt. The `customPrompt` field with [Verse]/[Chorus] structure works but isn't guaranteed to be followed precisely.