Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Enhanced fork of [gcui-art/suno-api](https://github.com/gcui-art/suno-api).

Adds an **admin dashboard**, **multi-account pool**, **global generation concurrency control**, **OpenAI-compatible endpoints with API key auth**, **credit and multiplier billing**, **YesCaptcha / 2Captcha**, and **cookie extraction tools**.

This fork adds a **local audio API** (upload your own audio and use it as a cover source), **Advanced Split** (per-instrument stems + MIDI export), and **download / export routes**.

[English](./README.md) · [简体中文](./README_CN.md)

---
Expand All @@ -22,6 +24,11 @@ Adds an **admin dashboard**, **multi-account pool**, **global generation concurr
- Credit and multiplier billing based on package cost, upstream credits, request usage, and group multiplier
- API key auth: `Authorization: Bearer <API_KEY>` (manage in admin UI)
- Captcha providers: YesCaptcha and 2Captcha (token-first)
- Local audio API:
- `POST /api/uploads/audio` — upload an audio file, get a `clip_id`
- Cover generation: pass `task: 'cover'` + `cover_clip_id` to `custom_generate`
- `GET /api/download` — quota-aware download/export (normal, Studio, sample-pack stems)
- Advanced Split: `POST /api/advanced_split` — per-instrument stems and MIDI for any song
- Cookie tools:
- Playwright extractor: `npm run get-cookie`
- Browser extension: `suno-cookie-extension`
Expand Down Expand Up @@ -371,6 +378,9 @@ Swagger UI: `/docs`

- `POST /api/generate`
- `POST /api/custom_generate`
- `POST /api/uploads/audio` (multipart: `file`, optional `mime_type`)
- `POST /api/advanced_split`
- `GET /api/download?id=<clip_id>&path=auto|normal|studio|sample-pack&format=mp3|wav&job=<job_id>`
- `POST /api/extend_audio`
- `POST /api/generate_lyrics`
- `GET /api/get?ids=...`
Expand All @@ -381,6 +391,56 @@ Swagger UI: `/docs`

---

## Fork Additions

### Local audio upload + cover generation

Upload a local audio file through the native Suno upload chain and get a `clip_id`:

```bash
curl -X POST http://localhost:3000/api/uploads/audio \
-F "file=@voice.mp3"
# -> { "clip_id": "...", ... }
```

Then generate a cover of it via `custom_generate`:

```json
{
"task": "cover",
"cover_clip_id": "<clip_id>",
"cover_start_s": 0,
"cover_end_s": 30,
"prompt": "..."
}
```

### Advanced Split (stems + MIDI)

Split any song into per-instrument stems, optionally exporting MIDI:

```json
POST /api/advanced_split
{
"song_id": "<clip_id>",
"instruments": ["vocals", "drums", "bass"],
"include_midi": true
}
```

The route proxies Suno's `v2-web` generation contract and polls the feed
until the split job finishes (requires a plan that has Advanced Split).

### Download / export routes

`GET /api/download?id=<clip_id>` picks the best quota path automatically:
Studio per-clip download and sample-pack (stems) exports do not count
against the monthly download quota on Premier plans, while the normal path
does. Use `path=` to force a specific path and `job=` to poll a running
sample-pack export.

---

## Environment Variables

| Variable | Required | Description |
Expand Down
86 changes: 86 additions & 0 deletions src/app/api/advanced_split/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers';
import { runSunoRequest } from "@/lib/SunoApi";
import { accountTier } from '@/lib/account-pool';
import { withGenerationConcurrency } from '@/lib/concurrency-settings';
import { concurrencyLimitResponse } from '@/lib/concurrency-response';
import { corsHeaders } from "@/lib/utils";

export const maxDuration = 60; // same as custom_generate; client method has its own deadlines
export const dynamic = "force-dynamic";

export async function POST(req: NextRequest) {
if (req.method === 'POST') {
try {
const body = await req.json().catch(() => ({}));
const { song_id, songId, audio_id, instruments, include_midi, mode } = body;
const sourceId = song_id || songId || audio_id;

if (!sourceId || typeof sourceId !== 'string') {
return new NextResponse(JSON.stringify({ error: 'song_id is required' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
if (
!Array.isArray(instruments)
|| instruments.length === 0
|| !instruments.every((i: unknown) => typeof i === 'string' && i.trim().length > 0)
) {
return new NextResponse(JSON.stringify({ error: 'instruments must be a non-empty array of strings' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}

const result = await withGenerationConcurrency(async () => runSunoRequest(
(await cookies()).toString(),
accountTier(body.pool || req.headers.get('x-suno-pool')),
(api) => api.advancedSplit(sourceId, {
instruments,
include_midi: Boolean(include_midi),
mode
}),
));
return new NextResponse(JSON.stringify(result), {
status: 200,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
} catch (error: any) {
console.error('Error running advanced split:', error);
const limited = concurrencyLimitResponse(error, corsHeaders);
if (limited) return limited;
return new NextResponse(JSON.stringify({ error: error.response?.data?.detail || error.toString() }), {
status: error.response?.status || 500,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
} else {
return new NextResponse('Method Not Allowed', {
headers: {
Allow: 'POST',
...corsHeaders
},
status: 405
});
}
}

export async function OPTIONS(request: Request) {
return new Response(null, {
status: 200,
headers: corsHeaders
});
}
56 changes: 56 additions & 0 deletions src/app/docs/swagger-suno-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,62 @@
"429": { "$ref": "#/components/responses/ConcurrencyLimitExceeded" }
}
}
},
"/api/advanced_split": {
"post": {
"summary": "Advanced Split: extract stems for chosen instruments (+ optional MIDI note-events).",
"description": "Wraps the Suno Studio \"Extract Stems and MIDI\" → Advanced split flow. One generation per instrument; each returns 4 clips (audio extract, audio remove, MIDI extract, MIDI remove). Audio stems are returned as cdn1 mp3 links once complete; MIDI note-events JSON is attached when include_midi is true. Files download via /api/download?path=studio.",
"tags": ["default"],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["song_id", "instruments"],
"properties": {
"song_id": { "type": "string", "description": "The clip to split.", "example": "7871d753-06ba-4110-b028-ccc1bb1e1ef9" },
"instruments": { "type": "array", "items": { "type": "string" }, "description": "Instrument names from the Suno dropdown (Lead Vocal, Drums, Bass, ...).", "example": ["Lead Vocal", "Drums"] },
"include_midi": { "type": "boolean", "description": "Also poll and attach MIDI note-events per instrument." },
"mode": { "type": "string", "description": "Echoed back in the response (defaults to advanced)." }
}
}
}
}
},
"responses": {
"200": {
"description": "Per-instrument split result",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"song_id": { "type": "string" },
"mode": { "type": "string" },
"instruments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"instrument": { "type": "string" },
"extract": { "type": "object", "properties": { "clip_id": { "type": "string" }, "title": { "type": "string" }, "status": { "type": "string" }, "audio_url": { "type": "string" } } },
"remove": { "type": "object", "properties": { "clip_id": { "type": "string" }, "title": { "type": "string" }, "status": { "type": "string" }, "audio_url": { "type": "string" } } },
"midi_extract": { "type": "object", "properties": { "clip_id": { "type": "string" }, "midi": { "type": "object" } } },
"midi_remove": { "type": "object", "properties": { "clip_id": { "type": "string" }, "midi": { "type": "object" } } }
}
}
}
}
}
}
}
},
"400": { "description": "Validation error (missing song_id / empty instruments)" },
"429": {
"$ref": "#/components/responses/ConcurrencyLimitExceeded"
}
}
}
},
"/api/generate_lyrics": {
"post": {
Expand Down
Loading