Merge pull request #33 from gcui-art/main
Merge into the release branch to publish the demo.
This commit is contained in:
commit
4cf051821e
14
FUNDING.yml
Normal file
14
FUNDING.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: gcui # Replace with a single Buy Me a Coffee username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
@ -13,7 +13,7 @@
|
|||||||
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api">Deploy with Vercel</a>
|
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api">Deploy with Vercel</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.producthunt.com/posts/gcui-art-suno-api-open-source-sunoai-api?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-gcui-art-suno-api-open-source-sunoai-api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=448029&theme=light" alt="gcui-art/suno-api:Open-source SunoAI API - Use API to call the music generation AI of suno.ai. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/products/gcui-art-suno-api-open-source-sunoai-api/reviews?utm_source=badge-product_review&utm_medium=badge&utm_souce=badge-gcui-art-suno-api-open-source-sunoai-api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/product_review.svg?product_id=577408&theme=light" alt="gcui-art/suno-api:Open-source SunoAI API - Use API to call the music generation AI of suno.ai. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|

|
||||||
@ -33,6 +33,7 @@ We have deployed an example bound to a free Suno account, so it has daily usage
|
|||||||
|
|
||||||
- Perfectly implements the creation API from app.suno.ai
|
- Perfectly implements the creation API from app.suno.ai
|
||||||
- Automatically keep the account active.
|
- Automatically keep the account active.
|
||||||
|
- Compatible with the format of OpenAI’s `/v1/chat/completions` API.
|
||||||
- Supports Custom Mode
|
- Supports Custom Mode
|
||||||
- One-click deployment to Vercel
|
- One-click deployment to Vercel
|
||||||
- In addition to the standard API, it also adapts to the API Schema of Agent platforms like GPTs and Coze, so you can use it as a tool/plugin/Action for LLMs and integrate it into any AI Agent.
|
- In addition to the standard API, it also adapts to the API Schema of Agent platforms like GPTs and Coze, so you can use it as a tool/plugin/Action for LLMs and integrate it into any AI Agent.
|
||||||
@ -116,6 +117,7 @@ Suno API currently mainly implements the following APIs:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
- `/api/generate`: Generate music
|
- `/api/generate`: Generate music
|
||||||
|
- `/v1/chat/completions`: Generate music - Call the generate API in a format that works with OpenAI’s API.
|
||||||
- `/api/custom_generate`: Generate music (Custom Mode, support setting lyrics, music style, title, etc.)
|
- `/api/custom_generate`: Generate music (Custom Mode, support setting lyrics, music style, title, etc.)
|
||||||
- `/api/generate_lyrics`: Generate lyrics based on prompt
|
- `/api/generate_lyrics`: Generate lyrics based on prompt
|
||||||
- `/api/get`: Get music information based on the id. Use “,” to separate multiple ids.
|
- `/api/get`: Get music information based on the id. Use “,” to separate multiple ids.
|
||||||
|
@ -14,8 +14,9 @@
|
|||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.producthunt.com/posts/gcui-art-suno-api-open-source-sunoai-api?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-gcui-art-suno-api-open-source-sunoai-api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=448029&theme=light" alt="gcui-art/suno-api:Open-source SunoAI API - Use API to call the music generation AI of suno.ai. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/products/gcui-art-suno-api-open-source-sunoai-api/reviews?utm_source=badge-product_review&utm_medium=badge&utm_souce=badge-gcui-art-suno-api-open-source-sunoai-api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/product_review.svg?product_id=577408&theme=light" alt="gcui-art/suno-api:Open-source SunoAI API - Use API to call the music generation AI of suno.ai. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
@ -32,6 +33,7 @@ Suno.ai v3 是一个令人惊叹的 AI 音乐服务,虽然官方还没有开
|
|||||||
|
|
||||||
- 完美的实现了 app.suno.ai 中的大部分 API
|
- 完美的实现了 app.suno.ai 中的大部分 API
|
||||||
- 自动保持账号活跃
|
- 自动保持账号活跃
|
||||||
|
- 兼容OpenAI的 `/v1/chat/completions` API 格式
|
||||||
- 支持 Custom Mode
|
- 支持 Custom Mode
|
||||||
- 一键部署到 vercel
|
- 一键部署到 vercel
|
||||||
- 除了标准 API,还适配了 GPTs、coze 等 Agent 平台的 API Schema,所以你可以把它当做一个 LLM 的工具/插件/Action,集成到任意 AI Agent 中。
|
- 除了标准 API,还适配了 GPTs、coze 等 Agent 平台的 API Schema,所以你可以把它当做一个 LLM 的工具/插件/Action,集成到任意 AI Agent 中。
|
||||||
@ -113,6 +115,7 @@ Suno API 目前主要实现了以下 API:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
- `/api/generate`: 创建音乐
|
- `/api/generate`: 创建音乐
|
||||||
|
- `/v1/chat/completions`: 创建音乐 - 用OpenAI API 兼容的格式调用 generate API
|
||||||
- `/api/custom_generate`: 创建音乐(自定义模式,支持设置歌词、音乐风格、设置标题等)
|
- `/api/custom_generate`: 创建音乐(自定义模式,支持设置歌词、音乐风格、设置标题等)
|
||||||
- `/api/generate_lyrics`: 根据Prompt创建歌词
|
- `/api/generate_lyrics`: 根据Prompt创建歌词
|
||||||
- `/api/get`: 根据id获取音乐信息。获取多个请用","分隔,不传ids则返回所有音乐
|
- `/api/get`: 根据id获取音乐信息。获取多个请用","分隔,不传ids则返回所有音乐
|
||||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "suno-api",
|
"name": "suno-api",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "suno-api",
|
"name": "suno-api",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/analytics": "^1.2.2",
|
"@vercel/analytics": "^1.2.2",
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
import { sunoApi } from "@/lib/SunoApi";
|
import { sunoApi } from "@/lib/SunoApi";
|
||||||
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
try {
|
try {
|
||||||
@ -9,7 +12,10 @@ export async function POST(req: NextRequest) {
|
|||||||
if (!prompt || !tags || !title) {
|
if (!prompt || !tags || !title) {
|
||||||
return new NextResponse(JSON.stringify({ error: 'Prompt, tags, and title are required' }), {
|
return new NextResponse(JSON.stringify({ error: 'Prompt, tags, and title are required' }), {
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const audioInfo = await (await sunoApi).custom_generate(
|
const audioInfo = await (await sunoApi).custom_generate(
|
||||||
@ -19,25 +25,44 @@ export async function POST(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
return new NextResponse(JSON.stringify(audioInfo), {
|
return new NextResponse(JSON.stringify(audioInfo), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error generating custom audio:', error.response.data);
|
console.error('Error generating custom audio:', error.response.data);
|
||||||
if (error.response.status === 402) {
|
if (error.response.status === 402) {
|
||||||
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
||||||
status: 402,
|
status: 402,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return new NextResponse('Method Not Allowed', {
|
return new NextResponse('Method Not Allowed', {
|
||||||
headers: { Allow: 'POST' },
|
headers: {
|
||||||
|
Allow: 'POST',
|
||||||
|
...corsHeaders
|
||||||
|
},
|
||||||
status: 405
|
status: 405
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function OPTIONS(request: Request) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: corsHeaders
|
||||||
|
});
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
import { sunoApi } from "@/lib/SunoApi";
|
import { sunoApi } from "@/lib/SunoApi";
|
||||||
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
try {
|
try {
|
||||||
@ -10,7 +13,10 @@ export async function POST(req: NextRequest) {
|
|||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
return new NextResponse(JSON.stringify({ error: 'Prompt is required' }), {
|
return new NextResponse(JSON.stringify({ error: 'Prompt is required' }), {
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,25 +24,45 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
return new NextResponse(JSON.stringify(audioInfo), {
|
return new NextResponse(JSON.stringify(audioInfo), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error generating custom audio:', JSON.stringify(error.response.data));
|
console.error('Error generating custom audio:', JSON.stringify(error.response.data));
|
||||||
if (error.response.status === 402) {
|
if (error.response.status === 402) {
|
||||||
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
||||||
status: 402,
|
status: 402,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return new NextResponse('Method Not Allowed', {
|
return new NextResponse('Method Not Allowed', {
|
||||||
headers: { Allow: 'POST' },
|
headers: {
|
||||||
|
Allow: 'POST',
|
||||||
|
...corsHeaders
|
||||||
|
},
|
||||||
status: 405
|
status: 405
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function OPTIONS(request: Request) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: corsHeaders
|
||||||
|
});
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
import { sunoApi } from "@/lib/SunoApi";
|
import { sunoApi } from "@/lib/SunoApi";
|
||||||
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
try {
|
try {
|
||||||
@ -10,7 +13,10 @@ export async function POST(req: NextRequest) {
|
|||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
return new NextResponse(JSON.stringify({ error: 'Prompt is required' }), {
|
return new NextResponse(JSON.stringify({ error: 'Prompt is required' }), {
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,25 +24,44 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
return new NextResponse(JSON.stringify(lyrics), {
|
return new NextResponse(JSON.stringify(lyrics), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error generating lyrics:', JSON.stringify(error.response.data));
|
console.error('Error generating lyrics:', JSON.stringify(error.response.data));
|
||||||
if (error.response.status === 402) {
|
if (error.response.status === 402) {
|
||||||
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
||||||
status: 402,
|
status: 402,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return new NextResponse('Method Not Allowed', {
|
return new NextResponse('Method Not Allowed', {
|
||||||
headers: { Allow: 'POST' },
|
headers: {
|
||||||
|
Allow: 'POST',
|
||||||
|
...corsHeaders
|
||||||
|
},
|
||||||
status: 405
|
status: 405
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function OPTIONS(request: Request) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: corsHeaders
|
||||||
|
});
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
import { sunoApi } from "@/lib/SunoApi";
|
import { sunoApi } from "@/lib/SunoApi";
|
||||||
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
@ -17,20 +19,36 @@ export async function GET(req: NextRequest) {
|
|||||||
|
|
||||||
return new NextResponse(JSON.stringify(audioInfo), {
|
return new NextResponse(JSON.stringify(audioInfo), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching audio:', error);
|
console.error('Error fetching audio:', error);
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return new NextResponse('Method Not Allowed', {
|
return new NextResponse('Method Not Allowed', {
|
||||||
headers: { Allow: 'GET' },
|
headers: {
|
||||||
|
Allow: 'GET',
|
||||||
|
...corsHeaders
|
||||||
|
},
|
||||||
status: 405
|
status: 405
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function OPTIONS(request: Request) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: corsHeaders
|
||||||
|
});
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
import { NextResponse, NextRequest } from "next/server";
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
import { sunoApi } from "@/lib/SunoApi";
|
import { sunoApi } from "@/lib/SunoApi";
|
||||||
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
try {
|
try {
|
||||||
@ -10,20 +13,36 @@ export async function GET(req: NextRequest) {
|
|||||||
|
|
||||||
return new NextResponse(JSON.stringify(limit), {
|
return new NextResponse(JSON.stringify(limit), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching limit:', error);
|
console.error('Error fetching limit:', error);
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify({ error: 'Internal server error. ' + error }), {
|
return new NextResponse(JSON.stringify({ error: 'Internal server error. ' + error }), {
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return new NextResponse('Method Not Allowed', {
|
return new NextResponse('Method Not Allowed', {
|
||||||
headers: { Allow: 'GET' },
|
headers: {
|
||||||
|
Allow: 'GET',
|
||||||
|
...corsHeaders
|
||||||
|
},
|
||||||
status: 405
|
status: 405
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function OPTIONS(request: Request) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: corsHeaders
|
||||||
|
});
|
||||||
|
}
|
@ -20,6 +20,8 @@ export default function Docs() {
|
|||||||
|
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
- \`/api/generate\`: Generate music
|
- \`/api/generate\`: Generate music
|
||||||
|
- \`/v1/chat/completions\`: Generate music - Call the generate API in a format
|
||||||
|
that works with OpenAI’s API.
|
||||||
- \`/api/custom_generate\`: Generate music (Custom Mode, support setting lyrics,
|
- \`/api/custom_generate\`: Generate music (Custom Mode, support setting lyrics,
|
||||||
music style, title, etc.)
|
music style, title, etc.)
|
||||||
- \`/api/generate_lyrics\`: Generate lyrics based on prompt
|
- \`/api/generate_lyrics\`: Generate lyrics based on prompt
|
||||||
|
@ -11,32 +11,22 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"/api/custom_generate": {
|
"/api/generate": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Generate Audio - Custom Mode",
|
"summary": "Generate audio based on Prompt.",
|
||||||
"description": "The custom mode enables users to provide additional details about the music, such as music genre, lyrics, and more.\n\n 2 audio files will be generated for each request, consuming a total of 10 credits. \n\n `wait_audio` can be set to API mode:\n\n\u2022 By default, it is set to false, which indicates the background mode. It will only return audio task information, and you will need to call the get API to retrieve detailed audio information.\n\n\u2022 If set to true, it simulates synchronous mode. The API will wait for a maximum of 100s until the audio is generated, and will directly return the audio link and other information. Recommend using in GPTs and other agents.",
|
"description": "It will automatically fill in the lyrics.\n\n2 audio files will be generated for each request, consuming a total of 10 credits.\n\n`wait_audio` can be set to API mode:\n\n\u2022 By default, it is set to `false`, which indicates the background mode. It will only return audio task information, and you will need to call the get API to retrieve detailed audio information.\n\n\u2022 If set to `true`, it simulates synchronous mode. The API will wait for a maximum of 100s until the audio is generated, and will directly return the audio link and other information. Recommend using in GPTs and other agents.",
|
||||||
"tags": ["default"],
|
"tags": ["default"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["prompt", "tags", "title"],
|
"required": ["prompt", "make_instrumental", "wait_audio"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Detailed prompt, including information such as music lyrics.",
|
"description": "Prompt",
|
||||||
"example": "[Verse 1]\nCruel flames of war engulf this land\nBattlefields filled with death and dread\nInnocent souls in darkness, they rest\nMy heart trembles in this silent test\n\n[Verse 2]\nPeople weep for loved ones lost\nBattered bodies bear the cost\nSeeking peace and hope once known\nOur grief transforms to hearts of stone\n\n[Chorus]\nSilent battlegrounds, no birds' song\nShadows of war, where we don't belong\nMay flowers of peace bloom in this place\nLet's guard this precious dream with grace\n\n[Bridge]\nThrough the ashes, we will rise\nHand in hand, towards peaceful skies\nNo more sorrow, no more pain\nTogether, we'll break these chains\n\n[Chorus]\nSilent battlegrounds, no birds' song\nShadows of war, where we don't belong\nMay flowers of peace bloom in this place\nLet's guard this precious dream with grace\n\n[Outro]\nIn unity, our strength will grow\nA brighter future, we'll soon know\nFrom the ruins, hope will spring\nA new dawn, we'll together bring"
|
"example": "A popular heavy metal song about war, sung by a deep-voiced male singer, slowly and melodiously. The lyrics depict the sorrow of people after the war."
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Music genre",
|
|
||||||
"example": "pop metal male melancholic"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Music title",
|
|
||||||
"example": "Silent Battlefield"
|
|
||||||
},
|
},
|
||||||
"make_instrumental": {
|
"make_instrumental": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@ -79,10 +69,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/generate": {
|
"/v1/chat/completions": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Generate audio based on Prompt.",
|
"summary": "Generate audio based on Prompt - OpenAI API format compatibility.",
|
||||||
"description": "It will automatically fill in the lyrics.\n\n2 audio files will be generated for each request, consuming a total of 10 credits.\n\n`wait_audio` can be set to API mode:\n\n\u2022 By default, it is set to `false`, which indicates the background mode. It will only return audio task information, and you will need to call the get API to retrieve detailed audio information.\n\n\u2022 If set to `true`, it simulates synchronous mode. The API will wait for a maximum of 100s until the audio is generated, and will directly return the audio link and other information. Recommend using in GPTs and other agents.",
|
"description": "Convert the `/api/generate` API to be compatible with the OpenAI `/v1/chat/completions` API format. \n\nGenerally used in OpenAI compatible clients.",
|
||||||
"tags": ["default"],
|
"tags": ["default"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"content": {
|
"content": {
|
||||||
@ -95,6 +85,58 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Prompt",
|
"description": "Prompt",
|
||||||
"example": "A popular heavy metal song about war, sung by a deep-voiced male singer, slowly and melodiously. The lyrics depict the sorrow of people after the war."
|
"example": "A popular heavy metal song about war, sung by a deep-voiced male singer, slowly and melodiously. The lyrics depict the sorrow of people after the war."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "success",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Text description for music, with details like title, album cover, lyrics, and more."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/custom_generate": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Generate Audio - Custom Mode",
|
||||||
|
"description": "The custom mode enables users to provide additional details about the music, such as music genre, lyrics, and more.\n\n 2 audio files will be generated for each request, consuming a total of 10 credits. \n\n `wait_audio` can be set to API mode:\n\n\u2022 By default, it is set to false, which indicates the background mode. It will only return audio task information, and you will need to call the get API to retrieve detailed audio information.\n\n\u2022 If set to true, it simulates synchronous mode. The API will wait for a maximum of 100s until the audio is generated, and will directly return the audio link and other information. Recommend using in GPTs and other agents.",
|
||||||
|
"tags": ["default"],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["prompt", "tags", "title"],
|
||||||
|
"properties": {
|
||||||
|
"prompt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Detailed prompt, including information such as music lyrics.",
|
||||||
|
"example": "[Verse 1]\nCruel flames of war engulf this land\nBattlefields filled with death and dread\nInnocent souls in darkness, they rest\nMy heart trembles in this silent test\n\n[Verse 2]\nPeople weep for loved ones lost\nBattered bodies bear the cost\nSeeking peace and hope once known\nOur grief transforms to hearts of stone\n\n[Chorus]\nSilent battlegrounds, no birds' song\nShadows of war, where we don't belong\nMay flowers of peace bloom in this place\nLet's guard this precious dream with grace\n\n[Bridge]\nThrough the ashes, we will rise\nHand in hand, towards peaceful skies\nNo more sorrow, no more pain\nTogether, we'll break these chains\n\n[Chorus]\nSilent battlegrounds, no birds' song\nShadows of war, where we don't belong\nMay flowers of peace bloom in this place\nLet's guard this precious dream with grace\n\n[Outro]\nIn unity, our strength will grow\nA brighter future, we'll soon know\nFrom the ruins, hope will spring\nA new dawn, we'll together bring"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Music genre",
|
||||||
|
"example": "pop metal male melancholic"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Music title",
|
||||||
|
"example": "Silent Battlefield"
|
||||||
},
|
},
|
||||||
"make_instrumental": {
|
"make_instrumental": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -18,6 +18,7 @@ We update quickly, please star us on Github: [github.com/gcui-art/suno-api](htt
|
|||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
- Perfectly implements the creation API from \`app.suno.ai\`
|
- Perfectly implements the creation API from \`app.suno.ai\`
|
||||||
|
- Compatible with the format of OpenAI’s \`/v1/chat/completions\` API.
|
||||||
- Automatically keep the account active.
|
- Automatically keep the account active.
|
||||||
- Supports \`Custom Mode\`
|
- Supports \`Custom Mode\`
|
||||||
- One-click deployment to Vercel
|
- One-click deployment to Vercel
|
||||||
@ -96,6 +97,8 @@ Suno API currently mainly implements the following APIs:
|
|||||||
|
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
- \`/api/generate\`: Generate music
|
- \`/api/generate\`: Generate music
|
||||||
|
- \`/v1/chat/completions\`: Generate music - Call the generate API in a format
|
||||||
|
that works with OpenAI’s API.
|
||||||
- \`/api/custom_generate\`: Generate music (Custom Mode, support setting lyrics,
|
- \`/api/custom_generate\`: Generate music (Custom Mode, support setting lyrics,
|
||||||
music style, title, etc.)
|
music style, title, etc.)
|
||||||
- \`/api/generate_lyrics\`: Generate lyrics based on prompt
|
- \`/api/generate_lyrics\`: Generate lyrics based on prompt
|
||||||
|
61
src/app/v1/chat/completions/route.ts
Normal file
61
src/app/v1/chat/completions/route.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { sunoApi } from "@/lib/SunoApi";
|
||||||
|
import { corsHeaders } from "@/lib/utils";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* desc
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
|
||||||
|
let userMessage = null;
|
||||||
|
const { messages } = body;
|
||||||
|
for (let message of messages) {
|
||||||
|
if (message.role == 'user') {
|
||||||
|
userMessage = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userMessage) {
|
||||||
|
return new NextResponse(JSON.stringify({ error: 'Prompt message is required' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const audioInfo = await (await sunoApi).generate(userMessage.content, true, true);
|
||||||
|
|
||||||
|
const audio = audioInfo[0]
|
||||||
|
const data = `## Song Title: ${audio.title}\n\n### Lyrics:\n${audio.lyric}\n### Listen to the song: ${audio.audio_url}`
|
||||||
|
|
||||||
|
return new NextResponse(data, {
|
||||||
|
status: 200,
|
||||||
|
headers: corsHeaders
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error generating audio:', JSON.stringify(error.response.data));
|
||||||
|
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function OPTIONS(request: Request) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: corsHeaders
|
||||||
|
});
|
||||||
|
}
|
@ -27,7 +27,7 @@ export interface AudioInfo {
|
|||||||
|
|
||||||
class SunoApi {
|
class SunoApi {
|
||||||
private static BASE_URL: string = 'https://studio-api.suno.ai';
|
private static BASE_URL: string = 'https://studio-api.suno.ai';
|
||||||
private static CLERK_BASE_URL: string = 'https://clerk.suno.ai';
|
private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
|
||||||
|
|
||||||
private readonly client: AxiosInstance;
|
private readonly client: AxiosInstance;
|
||||||
private sid?: string;
|
private sid?: string;
|
||||||
|
@ -19,3 +19,10 @@ export const sleep = (x: number, y?: number): Promise<void> => {
|
|||||||
|
|
||||||
return new Promise(resolve => setTimeout(resolve, timeout));
|
return new Promise(resolve => setTimeout(resolve, timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user