Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +43 -0
- .env.example +2 -0
- .eslintrc.json +3 -0
- .gitattributes +2 -0
- .github/ISSUE_TEMPLATE/bug_report.md +32 -0
- .github/ISSUE_TEMPLATE/feature_request.md +20 -0
- .github/ISSUE_TEMPLATE/新功能.md +21 -0
- .gitignore +39 -0
- Dockerfile +21 -0
- FUNDING.yml +14 -0
- LICENSE +165 -0
- README.md +327 -12
- README_CN.md +304 -0
- docker-compose.yml +12 -0
- icon.png +0 -0
- next.config.mjs +4 -0
- package.json +47 -0
- pnpm-lock.yaml +0 -0
- postcss.config.js +6 -0
- public/get-cookie-demo.gif +3 -0
- public/get-cookie-demo.mp4 +3 -0
- public/github-logo.webp +0 -0
- public/github-mark.png +0 -0
- public/next.svg +1 -0
- public/suno-banner.png +0 -0
- public/swagger-suno-api.json +257 -0
- public/vercel.svg +1 -0
- src/app/api/clip/route.ts +58 -0
- src/app/api/concat/route.ts +64 -0
- src/app/api/custom_generate/route.ts +61 -0
- src/app/api/extend_audio/route.ts +69 -0
- src/app/api/generate/route.ts +63 -0
- src/app/api/generate_lyrics/route.ts +57 -0
- src/app/api/get/route.ts +54 -0
- src/app/api/get_limit/route.ts +48 -0
- src/app/components/Footer.tsx +24 -0
- src/app/components/Header.tsx +50 -0
- src/app/components/Logo.tsx +13 -0
- src/app/components/Section.tsx +22 -0
- src/app/components/Swagger.tsx +15 -0
- src/app/docs/page.tsx +59 -0
- src/app/docs/swagger-suno-api.json +600 -0
- src/app/favicon.ico +0 -0
- src/app/globals.css +25 -0
- src/app/layout.tsx +34 -0
- src/app/page.tsx +154 -0
- src/app/v1/chat/completions/route.ts +61 -0
- src/lib/SunoApi.ts +407 -0
- src/lib/utils.ts +28 -0
- tailwind.config.ts +22 -0
.dockerignore
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
# dependencies
|
4 |
+
/node_modules
|
5 |
+
/.pnp
|
6 |
+
.pnp.js
|
7 |
+
.yarn/install-state.gz
|
8 |
+
|
9 |
+
# testing
|
10 |
+
/coverage
|
11 |
+
|
12 |
+
# next.js
|
13 |
+
/.next/
|
14 |
+
/out/
|
15 |
+
|
16 |
+
# production
|
17 |
+
/build
|
18 |
+
|
19 |
+
# misc
|
20 |
+
.DS_Store
|
21 |
+
*.pem
|
22 |
+
|
23 |
+
# debug
|
24 |
+
npm-debug.log*
|
25 |
+
yarn-debug.log*
|
26 |
+
yarn-error.log*
|
27 |
+
|
28 |
+
# local env files
|
29 |
+
.env*.local
|
30 |
+
|
31 |
+
# vercel
|
32 |
+
.vercel
|
33 |
+
|
34 |
+
# typescript
|
35 |
+
*.tsbuildinfo
|
36 |
+
next-env.d.ts
|
37 |
+
|
38 |
+
.idea
|
39 |
+
|
40 |
+
public/
|
41 |
+
Dockerfile
|
42 |
+
docker-compose.yml
|
43 |
+
README*.md
|
.env.example
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
SUNO_COOKIE=<your-suno-cookie>
|
2 |
+
|
.eslintrc.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "next/core-web-vitals"
|
3 |
+
}
|
.gitattributes
CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
public/get-cookie-demo.gif filter=lfs diff=lfs merge=lfs -text
|
37 |
+
public/get-cookie-demo.mp4 filter=lfs diff=lfs merge=lfs -text
|
.github/ISSUE_TEMPLATE/bug_report.md
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: Bug report
|
3 |
+
about: Create a report to help us improve
|
4 |
+
title: ''
|
5 |
+
labels: ''
|
6 |
+
assignees: ''
|
7 |
+
|
8 |
+
---
|
9 |
+
|
10 |
+
**Describe the bug**
|
11 |
+
A clear and concise description of what the bug is.
|
12 |
+
|
13 |
+
**To Reproduce**
|
14 |
+
Steps to reproduce the behavior:
|
15 |
+
1. Go to '...'
|
16 |
+
2. Click on '....'
|
17 |
+
3. Scroll down to '....'
|
18 |
+
4. See error
|
19 |
+
|
20 |
+
**Expected behavior**
|
21 |
+
A clear and concise description of what you expected to happen.
|
22 |
+
|
23 |
+
**Screenshots**
|
24 |
+
If applicable, add screenshots to help explain your problem.
|
25 |
+
|
26 |
+
**Desktop (please complete the following information):**
|
27 |
+
- OS: [e.g. iOS]
|
28 |
+
- Browser [e.g. chrome, safari]
|
29 |
+
- Version [e.g. 22]
|
30 |
+
|
31 |
+
**Additional context**
|
32 |
+
Add any other context about the problem here.
|
.github/ISSUE_TEMPLATE/feature_request.md
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: Feature request
|
3 |
+
about: Suggest an idea for this project
|
4 |
+
title: ''
|
5 |
+
labels: ''
|
6 |
+
assignees: ''
|
7 |
+
|
8 |
+
---
|
9 |
+
|
10 |
+
**Is your feature request related to a problem? Please describe.**
|
11 |
+
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
12 |
+
|
13 |
+
**Describe the solution you'd like**
|
14 |
+
A clear and concise description of what you want to happen.
|
15 |
+
|
16 |
+
**Describe alternatives you've considered**
|
17 |
+
A clear and concise description of any alternative solutions or features you've considered.
|
18 |
+
|
19 |
+
**Additional context**
|
20 |
+
Add any other context or screenshots about the feature request here.
|
.github/ISSUE_TEMPLATE/新功能.md
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: 新功能
|
3 |
+
about: 为这个项目提出一个新想法 / 需求
|
4 |
+
title: 我需要一个...功能,来解决...问题
|
5 |
+
labels: ''
|
6 |
+
assignees: ''
|
7 |
+
|
8 |
+
---
|
9 |
+
|
10 |
+
**请描述您想要的新功能,可以解决什么问题?**
|
11 |
+
对被解决的问题明确而简洁的描述。例:
|
12 |
+
当...时,我感觉很沮丧。
|
13 |
+
|
14 |
+
**描述你想要的新功能**
|
15 |
+
清晰而简洁的描述你想要的新功能是什么。
|
16 |
+
|
17 |
+
**描述你考虑过的替代方案**
|
18 |
+
请清晰的简洁的描述你考虑过的任何替代方案,这将有助于我们理解你的需求。
|
19 |
+
|
20 |
+
**其他有关这个新功能的信息**
|
21 |
+
在这里添加更多的有关这个新功能的上下文,或者截图。
|
.gitignore
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
# dependencies
|
4 |
+
/node_modules
|
5 |
+
/.pnp
|
6 |
+
.pnp.js
|
7 |
+
.yarn/install-state.gz
|
8 |
+
|
9 |
+
# testing
|
10 |
+
/coverage
|
11 |
+
|
12 |
+
# next.js
|
13 |
+
/.next/
|
14 |
+
/out/
|
15 |
+
|
16 |
+
# production
|
17 |
+
/build
|
18 |
+
|
19 |
+
# misc
|
20 |
+
.DS_Store
|
21 |
+
*.pem
|
22 |
+
|
23 |
+
# debug
|
24 |
+
npm-debug.log*
|
25 |
+
yarn-debug.log*
|
26 |
+
yarn-error.log*
|
27 |
+
|
28 |
+
# local env files
|
29 |
+
.env*.local
|
30 |
+
.env
|
31 |
+
|
32 |
+
# vercel
|
33 |
+
.vercel
|
34 |
+
|
35 |
+
# typescript
|
36 |
+
*.tsbuildinfo
|
37 |
+
next-env.d.ts
|
38 |
+
|
39 |
+
.idea
|
Dockerfile
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# syntax=docker/dockerfile:1
|
2 |
+
|
3 |
+
FROM node:lts-alpine AS builder
|
4 |
+
WORKDIR /src
|
5 |
+
COPY package*.json ./
|
6 |
+
RUN npm install
|
7 |
+
COPY . .
|
8 |
+
RUN npm run build
|
9 |
+
|
10 |
+
FROM node:lts-alpine
|
11 |
+
WORKDIR /app
|
12 |
+
COPY package*.json ./
|
13 |
+
|
14 |
+
ARG SUNO_COOKIE
|
15 |
+
RUN if [ -z "$SUNO_COOKIE" ]; then echo "SUNO_COOKIE is not set" && exit 1; fi
|
16 |
+
ENV SUNO_COOKIE=${SUNO_COOKIE}
|
17 |
+
|
18 |
+
RUN npm install --only=production
|
19 |
+
COPY --from=builder /src/.next ./.next
|
20 |
+
EXPOSE 3000
|
21 |
+
CMD ["npm", "run", "start"]
|
FUNDING.yml
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# These are supported funding model platforms
|
2 |
+
|
3 |
+
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
4 |
+
patreon: # Replace with a single Patreon username
|
5 |
+
open_collective: # Replace with a single Open Collective username
|
6 |
+
ko_fi: # Replace with a single Ko-fi username
|
7 |
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
8 |
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
9 |
+
liberapay: # Replace with a single Liberapay username
|
10 |
+
issuehunt: # Replace with a single IssueHunt username
|
11 |
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
12 |
+
polar: # Replace with a single Polar username
|
13 |
+
buy_me_a_coffee: gcui # Replace with a single Buy Me a Coffee username
|
14 |
+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
LICENSE
ADDED
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
GNU LESSER GENERAL PUBLIC LICENSE
|
2 |
+
Version 3, 29 June 2007
|
3 |
+
|
4 |
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
5 |
+
Everyone is permitted to copy and distribute verbatim copies
|
6 |
+
of this license document, but changing it is not allowed.
|
7 |
+
|
8 |
+
|
9 |
+
This version of the GNU Lesser General Public License incorporates
|
10 |
+
the terms and conditions of version 3 of the GNU General Public
|
11 |
+
License, supplemented by the additional permissions listed below.
|
12 |
+
|
13 |
+
0. Additional Definitions.
|
14 |
+
|
15 |
+
As used herein, "this License" refers to version 3 of the GNU Lesser
|
16 |
+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
17 |
+
General Public License.
|
18 |
+
|
19 |
+
"The Library" refers to a covered work governed by this License,
|
20 |
+
other than an Application or a Combined Work as defined below.
|
21 |
+
|
22 |
+
An "Application" is any work that makes use of an interface provided
|
23 |
+
by the Library, but which is not otherwise based on the Library.
|
24 |
+
Defining a subclass of a class defined by the Library is deemed a mode
|
25 |
+
of using an interface provided by the Library.
|
26 |
+
|
27 |
+
A "Combined Work" is a work produced by combining or linking an
|
28 |
+
Application with the Library. The particular version of the Library
|
29 |
+
with which the Combined Work was made is also called the "Linked
|
30 |
+
Version".
|
31 |
+
|
32 |
+
The "Minimal Corresponding Source" for a Combined Work means the
|
33 |
+
Corresponding Source for the Combined Work, excluding any source code
|
34 |
+
for portions of the Combined Work that, considered in isolation, are
|
35 |
+
based on the Application, and not on the Linked Version.
|
36 |
+
|
37 |
+
The "Corresponding Application Code" for a Combined Work means the
|
38 |
+
object code and/or source code for the Application, including any data
|
39 |
+
and utility programs needed for reproducing the Combined Work from the
|
40 |
+
Application, but excluding the System Libraries of the Combined Work.
|
41 |
+
|
42 |
+
1. Exception to Section 3 of the GNU GPL.
|
43 |
+
|
44 |
+
You may convey a covered work under sections 3 and 4 of this License
|
45 |
+
without being bound by section 3 of the GNU GPL.
|
46 |
+
|
47 |
+
2. Conveying Modified Versions.
|
48 |
+
|
49 |
+
If you modify a copy of the Library, and, in your modifications, a
|
50 |
+
facility refers to a function or data to be supplied by an Application
|
51 |
+
that uses the facility (other than as an argument passed when the
|
52 |
+
facility is invoked), then you may convey a copy of the modified
|
53 |
+
version:
|
54 |
+
|
55 |
+
a) under this License, provided that you make a good faith effort to
|
56 |
+
ensure that, in the event an Application does not supply the
|
57 |
+
function or data, the facility still operates, and performs
|
58 |
+
whatever part of its purpose remains meaningful, or
|
59 |
+
|
60 |
+
b) under the GNU GPL, with none of the additional permissions of
|
61 |
+
this License applicable to that copy.
|
62 |
+
|
63 |
+
3. Object Code Incorporating Material from Library Header Files.
|
64 |
+
|
65 |
+
The object code form of an Application may incorporate material from
|
66 |
+
a header file that is part of the Library. You may convey such object
|
67 |
+
code under terms of your choice, provided that, if the incorporated
|
68 |
+
material is not limited to numerical parameters, data structure
|
69 |
+
layouts and accessors, or small macros, inline functions and templates
|
70 |
+
(ten or fewer lines in length), you do both of the following:
|
71 |
+
|
72 |
+
a) Give prominent notice with each copy of the object code that the
|
73 |
+
Library is used in it and that the Library and its use are
|
74 |
+
covered by this License.
|
75 |
+
|
76 |
+
b) Accompany the object code with a copy of the GNU GPL and this license
|
77 |
+
document.
|
78 |
+
|
79 |
+
4. Combined Works.
|
80 |
+
|
81 |
+
You may convey a Combined Work under terms of your choice that,
|
82 |
+
taken together, effectively do not restrict modification of the
|
83 |
+
portions of the Library contained in the Combined Work and reverse
|
84 |
+
engineering for debugging such modifications, if you also do each of
|
85 |
+
the following:
|
86 |
+
|
87 |
+
a) Give prominent notice with each copy of the Combined Work that
|
88 |
+
the Library is used in it and that the Library and its use are
|
89 |
+
covered by this License.
|
90 |
+
|
91 |
+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
92 |
+
document.
|
93 |
+
|
94 |
+
c) For a Combined Work that displays copyright notices during
|
95 |
+
execution, include the copyright notice for the Library among
|
96 |
+
these notices, as well as a reference directing the user to the
|
97 |
+
copies of the GNU GPL and this license document.
|
98 |
+
|
99 |
+
d) Do one of the following:
|
100 |
+
|
101 |
+
0) Convey the Minimal Corresponding Source under the terms of this
|
102 |
+
License, and the Corresponding Application Code in a form
|
103 |
+
suitable for, and under terms that permit, the user to
|
104 |
+
recombine or relink the Application with a modified version of
|
105 |
+
the Linked Version to produce a modified Combined Work, in the
|
106 |
+
manner specified by section 6 of the GNU GPL for conveying
|
107 |
+
Corresponding Source.
|
108 |
+
|
109 |
+
1) Use a suitable shared library mechanism for linking with the
|
110 |
+
Library. A suitable mechanism is one that (a) uses at run time
|
111 |
+
a copy of the Library already present on the user's computer
|
112 |
+
system, and (b) will operate properly with a modified version
|
113 |
+
of the Library that is interface-compatible with the Linked
|
114 |
+
Version.
|
115 |
+
|
116 |
+
e) Provide Installation Information, but only if you would otherwise
|
117 |
+
be required to provide such information under section 6 of the
|
118 |
+
GNU GPL, and only to the extent that such information is
|
119 |
+
necessary to install and execute a modified version of the
|
120 |
+
Combined Work produced by recombining or relinking the
|
121 |
+
Application with a modified version of the Linked Version. (If
|
122 |
+
you use option 4d0, the Installation Information must accompany
|
123 |
+
the Minimal Corresponding Source and Corresponding Application
|
124 |
+
Code. If you use option 4d1, you must provide the Installation
|
125 |
+
Information in the manner specified by section 6 of the GNU GPL
|
126 |
+
for conveying Corresponding Source.)
|
127 |
+
|
128 |
+
5. Combined Libraries.
|
129 |
+
|
130 |
+
You may place library facilities that are a work based on the
|
131 |
+
Library side by side in a single library together with other library
|
132 |
+
facilities that are not Applications and are not covered by this
|
133 |
+
License, and convey such a combined library under terms of your
|
134 |
+
choice, if you do both of the following:
|
135 |
+
|
136 |
+
a) Accompany the combined library with a copy of the same work based
|
137 |
+
on the Library, uncombined with any other library facilities,
|
138 |
+
conveyed under the terms of this License.
|
139 |
+
|
140 |
+
b) Give prominent notice with the combined library that part of it
|
141 |
+
is a work based on the Library, and explaining where to find the
|
142 |
+
accompanying uncombined form of the same work.
|
143 |
+
|
144 |
+
6. Revised Versions of the GNU Lesser General Public License.
|
145 |
+
|
146 |
+
The Free Software Foundation may publish revised and/or new versions
|
147 |
+
of the GNU Lesser General Public License from time to time. Such new
|
148 |
+
versions will be similar in spirit to the present version, but may
|
149 |
+
differ in detail to address new problems or concerns.
|
150 |
+
|
151 |
+
Each version is given a distinguishing version number. If the
|
152 |
+
Library as you received it specifies that a certain numbered version
|
153 |
+
of the GNU Lesser General Public License "or any later version"
|
154 |
+
applies to it, you have the option of following the terms and
|
155 |
+
conditions either of that published version or of any later version
|
156 |
+
published by the Free Software Foundation. If the Library as you
|
157 |
+
received it does not specify a version number of the GNU Lesser
|
158 |
+
General Public License, you may choose any version of the GNU Lesser
|
159 |
+
General Public License ever published by the Free Software Foundation.
|
160 |
+
|
161 |
+
If the Library as you received it specifies that a proxy can decide
|
162 |
+
whether future versions of the GNU Lesser General Public License shall
|
163 |
+
apply, that proxy's public statement of acceptance of any version is
|
164 |
+
permanent authorization for you to choose that version for the
|
165 |
+
Library.
|
README.md
CHANGED
@@ -1,12 +1,327 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<div align="center">
|
2 |
+
<h1 align="center"">
|
3 |
+
Suno AI API
|
4 |
+
</h1>
|
5 |
+
<p>Use API to call the music generation AI of Suno.ai and easily integrate it into agents like GPTs.</p>
|
6 |
+
<p>👉 We update quickly, please star.</p>
|
7 |
+
</div>
|
8 |
+
<p align="center">
|
9 |
+
<a target="_blank" href="./README.md">English</a>
|
10 |
+
| <a target="_blank" href="./README_CN.md">简体中文</a>
|
11 |
+
| <a target="_blank" href="https://suno.gcui.ai">Demo</a>
|
12 |
+
| <a target="_blank" href="https://suno.gcui.ai/docs">Docs</a>
|
13 |
+
| <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>
|
14 |
+
</p>
|
15 |
+
<p align="center">
|
16 |
+
<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>
|
17 |
+
</p>
|
18 |
+
|
19 |
+
> 🔥 Check out our new open-source project: [Album AI - Chat with your gallery using plain language!](https://github.com/gcui-art/album-ai)
|
20 |
+
|
21 |
+

|
22 |
+
|
23 |
+
## Introduction
|
24 |
+
|
25 |
+
Suno.ai v3 is an amazing AI music service. Although the official API is not yet available, we couldn't wait to integrate its capabilities somewhere.
|
26 |
+
|
27 |
+
We discovered that some users have similar needs, so we decided to open-source this project, hoping you'll like it.
|
28 |
+
|
29 |
+
## Demo
|
30 |
+
|
31 |
+
We have deployed an example bound to a free Suno account, so it has daily usage limits, but you can see how it runs:
|
32 |
+
[suno.gcui.ai](https://suno.gcui.ai)
|
33 |
+
|
34 |
+
## Features
|
35 |
+
|
36 |
+
- Perfectly implements the creation API from app.suno.ai
|
37 |
+
- Automatically keep the account active.
|
38 |
+
- Compatible with the format of OpenAI’s `/v1/chat/completions` API.
|
39 |
+
- Supports Custom Mode
|
40 |
+
- One-click deployment to Vercel
|
41 |
+
- 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.
|
42 |
+
- Permissive open-source license, allowing you to freely integrate and modify.
|
43 |
+
|
44 |
+
## Getting Started
|
45 |
+
|
46 |
+
### 1. Obtain the cookie of your app.suno.ai account
|
47 |
+
|
48 |
+
1. Head over to [app.suno.ai](https://app.suno.ai) using your browser.
|
49 |
+
2. Open up the browser console: hit `F12` or access the `Developer Tools`.
|
50 |
+
3. Navigate to the `Network tab`.
|
51 |
+
4. Give the page a quick refresh.
|
52 |
+
5. Identify the request that includes the keyword `client?_clerk_js_version`.
|
53 |
+
6. Click on it and switch over to the `Header` tab.
|
54 |
+
7. Locate the `Cookie` section, hover your mouse over it, and copy the value of the Cookie.
|
55 |
+
|
56 |
+

|
57 |
+
|
58 |
+
### 2. Clone and deploy this project
|
59 |
+
|
60 |
+
You can choose your preferred deployment method:
|
61 |
+
|
62 |
+
#### Deploy to Vercel
|
63 |
+
|
64 |
+
[](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)
|
65 |
+
|
66 |
+
#### Run locally
|
67 |
+
|
68 |
+
```bash
|
69 |
+
git clone https://github.com/gcui-art/suno-api.git
|
70 |
+
cd suno-api
|
71 |
+
npm install
|
72 |
+
```
|
73 |
+
|
74 |
+
Alternatively, you can use [Docker Compose](https://docs.docker.com/compose/)
|
75 |
+
|
76 |
+
```bash
|
77 |
+
docker compose build && docker compose up
|
78 |
+
```
|
79 |
+
|
80 |
+
### 3. Configure suno-api
|
81 |
+
|
82 |
+
- If deployed to Vercel, please add an environment variable `SUNO_COOKIE` in the Vercel dashboard, with the value of the cookie obtained in the first step.
|
83 |
+
|
84 |
+
- If you’re running this locally, be sure to add the following to your `.env` file:
|
85 |
+
|
86 |
+
```bash
|
87 |
+
SUNO_COOKIE=<your-cookie>
|
88 |
+
```
|
89 |
+
|
90 |
+
### 4. Run suno api
|
91 |
+
|
92 |
+
- If you’ve deployed to Vercel:
|
93 |
+
- Please click on Deploy in the Vercel dashboard and wait for the deployment to be successful.
|
94 |
+
- Visit the `https://<vercel-assigned-domain>/api/get_limit` API for testing.
|
95 |
+
- If running locally:
|
96 |
+
- Run `npm run dev`.
|
97 |
+
- Visit the `http://localhost:3000/api/get_limit` API for testing.
|
98 |
+
- If the following result is returned:
|
99 |
+
|
100 |
+
```json
|
101 |
+
{
|
102 |
+
"credits_left": 50,
|
103 |
+
"period": "day",
|
104 |
+
"monthly_limit": 50,
|
105 |
+
"monthly_usage": 50
|
106 |
+
}
|
107 |
+
```
|
108 |
+
|
109 |
+
it means the program is running normally.
|
110 |
+
|
111 |
+
### 5. Use Suno API
|
112 |
+
|
113 |
+
You can check out the detailed API documentation at :
|
114 |
+
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
115 |
+
|
116 |
+
## API Reference
|
117 |
+
|
118 |
+
Suno API currently mainly implements the following APIs:
|
119 |
+
|
120 |
+
```bash
|
121 |
+
- `/api/generate`: Generate music
|
122 |
+
- `/v1/chat/completions`: Generate music - Call the generate API in a format that works with OpenAI’s API.
|
123 |
+
- `/api/custom_generate`: Generate music (Custom Mode, support setting lyrics, music style, title, etc.)
|
124 |
+
- `/api/generate_lyrics`: Generate lyrics based on prompt
|
125 |
+
- `/api/get`: Get music information based on the id. Use “,” to separate multiple ids.
|
126 |
+
If no IDs are provided, all music will be returned.
|
127 |
+
- `/api/get_limit`: Get quota Info
|
128 |
+
- `/api/extend_audio`: Extend audio length
|
129 |
+
- `/api/clip`: Get clip information based on ID passed as query parameter `id`
|
130 |
+
- `/api/concat`: Generate the whole song from extensions
|
131 |
+
```
|
132 |
+
|
133 |
+
For more detailed documentation, please check out the demo site:
|
134 |
+
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
135 |
+
|
136 |
+
## API Integration Code Example
|
137 |
+
|
138 |
+
### Python
|
139 |
+
|
140 |
+
```python
|
141 |
+
import time
|
142 |
+
import requests
|
143 |
+
|
144 |
+
# replace your vercel domain
|
145 |
+
base_url = 'http://localhost:3000'
|
146 |
+
|
147 |
+
|
148 |
+
def custom_generate_audio(payload):
|
149 |
+
url = f"{base_url}/api/custom_generate"
|
150 |
+
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
|
151 |
+
return response.json()
|
152 |
+
|
153 |
+
|
154 |
+
def extend_audio(payload):
|
155 |
+
url = f"{base_url}/api/extend_audio"
|
156 |
+
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
|
157 |
+
return response.json()
|
158 |
+
|
159 |
+
def generate_audio_by_prompt(payload):
|
160 |
+
url = f"{base_url}/api/generate"
|
161 |
+
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
|
162 |
+
return response.json()
|
163 |
+
|
164 |
+
|
165 |
+
def get_audio_information(audio_ids):
|
166 |
+
url = f"{base_url}/api/get?ids={audio_ids}"
|
167 |
+
response = requests.get(url)
|
168 |
+
return response.json()
|
169 |
+
|
170 |
+
|
171 |
+
def get_quota_information():
|
172 |
+
url = f"{base_url}/api/get_limit"
|
173 |
+
response = requests.get(url)
|
174 |
+
return response.json()
|
175 |
+
|
176 |
+
def get_clip(clip_id):
|
177 |
+
url = f"{base_url}/api/clip?id={clip_id}"
|
178 |
+
response = requests.get(url)
|
179 |
+
return response.json()
|
180 |
+
|
181 |
+
def generate_whole_song(clip_id):
|
182 |
+
payloyd = {"clip_id": clip_id}
|
183 |
+
url = f"{base_url}/api/concat"
|
184 |
+
response = requests.post(url, json=payload)
|
185 |
+
return response.json()
|
186 |
+
|
187 |
+
|
188 |
+
if __name__ == '__main__':
|
189 |
+
data = generate_audio_by_prompt({
|
190 |
+
"prompt": "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.",
|
191 |
+
"make_instrumental": False,
|
192 |
+
"wait_audio": False
|
193 |
+
})
|
194 |
+
|
195 |
+
ids = f"{data[0]['id']},{data[1]['id']}"
|
196 |
+
print(f"ids: {ids}")
|
197 |
+
|
198 |
+
for _ in range(60):
|
199 |
+
data = get_audio_information(ids)
|
200 |
+
if data[0]["status"] == 'streaming':
|
201 |
+
print(f"{data[0]['id']} ==> {data[0]['audio_url']}")
|
202 |
+
print(f"{data[1]['id']} ==> {data[1]['audio_url']}")
|
203 |
+
break
|
204 |
+
# sleep 5s
|
205 |
+
time.sleep(5)
|
206 |
+
|
207 |
+
```
|
208 |
+
|
209 |
+
### Js
|
210 |
+
|
211 |
+
```js
|
212 |
+
const axios = require("axios");
|
213 |
+
|
214 |
+
// replace your vercel domain
|
215 |
+
const baseUrl = "http://localhost:3000";
|
216 |
+
|
217 |
+
async function customGenerateAudio(payload) {
|
218 |
+
const url = `${baseUrl}/api/custom_generate`;
|
219 |
+
const response = await axios.post(url, payload, {
|
220 |
+
headers: { "Content-Type": "application/json" },
|
221 |
+
});
|
222 |
+
return response.data;
|
223 |
+
}
|
224 |
+
|
225 |
+
async function generateAudioByPrompt(payload) {
|
226 |
+
const url = `${baseUrl}/api/generate`;
|
227 |
+
const response = await axios.post(url, payload, {
|
228 |
+
headers: { "Content-Type": "application/json" },
|
229 |
+
});
|
230 |
+
return response.data;
|
231 |
+
}
|
232 |
+
|
233 |
+
async function extendAudio(payload) {
|
234 |
+
const url = `${baseUrl}/api/extend_audio`;
|
235 |
+
const response = await axios.post(url, payload, {
|
236 |
+
headers: { "Content-Type": "application/json" },
|
237 |
+
});
|
238 |
+
return response.data;
|
239 |
+
}
|
240 |
+
|
241 |
+
async function getAudioInformation(audioIds) {
|
242 |
+
const url = `${baseUrl}/api/get?ids=${audioIds}`;
|
243 |
+
const response = await axios.get(url);
|
244 |
+
return response.data;
|
245 |
+
}
|
246 |
+
|
247 |
+
async function getQuotaInformation() {
|
248 |
+
const url = `${baseUrl}/api/get_limit`;
|
249 |
+
const response = await axios.get(url);
|
250 |
+
return response.data;
|
251 |
+
}
|
252 |
+
|
253 |
+
async function getClipInformation(clipId) {
|
254 |
+
const url = `${baseUrl}/api/clip?id=${clipId}`;
|
255 |
+
const response = await axios.get(url);
|
256 |
+
return response.data;
|
257 |
+
}
|
258 |
+
|
259 |
+
async function main() {
|
260 |
+
const data = await generateAudioByPrompt({
|
261 |
+
prompt:
|
262 |
+
"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.",
|
263 |
+
make_instrumental: false,
|
264 |
+
wait_audio: false,
|
265 |
+
});
|
266 |
+
|
267 |
+
const ids = `${data[0].id},${data[1].id}`;
|
268 |
+
console.log(`ids: ${ids}`);
|
269 |
+
|
270 |
+
for (let i = 0; i < 60; i++) {
|
271 |
+
const data = await getAudioInformation(ids);
|
272 |
+
if (data[0].status === "streaming") {
|
273 |
+
console.log(`${data[0].id} ==> ${data[0].audio_url}`);
|
274 |
+
console.log(`${data[1].id} ==> ${data[1].audio_url}`);
|
275 |
+
break;
|
276 |
+
}
|
277 |
+
// sleep 5s
|
278 |
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
279 |
+
}
|
280 |
+
}
|
281 |
+
|
282 |
+
main();
|
283 |
+
```
|
284 |
+
|
285 |
+
## Integration with Custom Agents
|
286 |
+
|
287 |
+
You can integrate Suno AI as a tool/plugin/action into your AI agent.
|
288 |
+
|
289 |
+
### Integration with GPTs
|
290 |
+
|
291 |
+
[coming soon...]
|
292 |
+
|
293 |
+
### Integration with Coze
|
294 |
+
|
295 |
+
[coming soon...]
|
296 |
+
|
297 |
+
### Integration with LangChain
|
298 |
+
|
299 |
+
[coming soon...]
|
300 |
+
|
301 |
+
## Contributing
|
302 |
+
|
303 |
+
There are four ways you can support this project:
|
304 |
+
|
305 |
+
1. Fork and Submit Pull Requests: We welcome any PRs that enhance the component or editor.
|
306 |
+
2. Open Issues: We appreciate reasonable suggestions and bug reports.
|
307 |
+
3. Donate: If this project has helped you, consider buying us a coffee using the Sponsor button at the top of the project. Cheers! ☕
|
308 |
+
4. Spread the Word: Recommend this project to others, star the repo, or add a backlink after using the project.
|
309 |
+
|
310 |
+
## Questions, Suggestions, Issues, or Bugs?
|
311 |
+
|
312 |
+
We use GitHub Issues to manage feedback. Feel free to open an issue, and we'll address it promptly.
|
313 |
+
|
314 |
+
## License
|
315 |
+
|
316 |
+
LGPL-3.0 or later
|
317 |
+
|
318 |
+
## Related Links
|
319 |
+
|
320 |
+
- Project repository: [github.com/gcui-art/suno-api](https://github.com/gcui-art/suno-api)
|
321 |
+
- Suno.ai official website: [suno.ai](https://suno.ai)
|
322 |
+
- Demo: [suno.gcui.ai](https://suno.gcui.ai)
|
323 |
+
- Album AI: [Auto generate image metadata and chat with the album. RAG + Album.](https://github.com/gcui-art/album-ai)
|
324 |
+
|
325 |
+
## Statement
|
326 |
+
|
327 |
+
suno-api is an unofficial open source project, intended for learning and research purposes only.
|
README_CN.md
ADDED
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<div align="center">
|
2 |
+
<h1 align="center"">
|
3 |
+
Suno AI API
|
4 |
+
</h1>
|
5 |
+
<p>用 API 调用 suno.ai 的音乐生成 AI,并且可以轻松集成到 GPTs 等 agent 中。</p>
|
6 |
+
<p>👉 我们更新很快,欢迎 star。</p>
|
7 |
+
</div>
|
8 |
+
<p align="center">
|
9 |
+
<a target="_blank" href="./README.md">English</a>
|
10 |
+
| <a target="_blank" href="./README_CN.md">简体中文</a>
|
11 |
+
| <a target="_blank" href="https://suno.gcui.ai">Demo</a>
|
12 |
+
| <a target="_blank" href="https://suno.gcui.ai/docs">文档</a>
|
13 |
+
| <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">一键部署到 Vercel</a>
|
14 |
+
|
15 |
+
</p>
|
16 |
+
<p align="center">
|
17 |
+
<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>
|
18 |
+
</p>
|
19 |
+
> 🔥 我们新的开源项目: [Album AI - 用自然语言与你的图库对话!](https://github.com/gcui-art/album-ai)
|
20 |
+
|
21 |
+

|
22 |
+
|
23 |
+
## 简介
|
24 |
+
|
25 |
+
Suno.ai v3 是一个令人惊叹的 AI 音乐服务,虽然官方还没有开放 API,但我们已经迫不及待的想在某些地方集成它的能力。
|
26 |
+
我们发现有一些用户也有类似需求,于是我们将这个项目开源了,希望你们喜欢。
|
27 |
+
|
28 |
+
## Demo
|
29 |
+
|
30 |
+
我们部署了一个示例,绑定了一个免费的 suno 账号,所以它每天有使用限制,但你可以看到它运行起来的样子:
|
31 |
+
[suno.gcui.ai](https://suno.gcui.ai)
|
32 |
+
|
33 |
+
## Features
|
34 |
+
|
35 |
+
- 完美的实现了 app.suno.ai 中的大部分 API
|
36 |
+
- 自动保持账号活跃
|
37 |
+
- 兼容 OpenAI 的 `/v1/chat/completions` API 格式
|
38 |
+
- 支持 Custom Mode
|
39 |
+
- 一键部署到 vercel
|
40 |
+
- 除了标准 API,还适配了 GPTs、coze 等 Agent 平台的 API Schema,所以你可以把它当做一个 LLM 的工具/插件/Action,集成到任意 AI Agent 中。
|
41 |
+
- 宽松的开源协议,你可以随意的集成和修改。
|
42 |
+
|
43 |
+
## 如何开始使用?
|
44 |
+
|
45 |
+
### 1. 获取你的 app.suno.ai 账号的 cookie
|
46 |
+
|
47 |
+
1. 浏览器访问 [app.suno.ai](https://app.suno.ai)
|
48 |
+
2. 打开浏览器的控制台:按下 `F12` 或者`开发者工具`
|
49 |
+
3. 选择`网络`标签
|
50 |
+
4. 刷新页面
|
51 |
+
5. 找到包含`client?_clerk_js_version`关键词的请求
|
52 |
+
6. 点击并切换到 `Header` 标签
|
53 |
+
7. 找到 `Cookie` 部分,鼠标复制 Cookie 的值
|
54 |
+
|
55 |
+

|
56 |
+
|
57 |
+
### 2. 克隆并部署本项目
|
58 |
+
|
59 |
+
你可以选择自己喜欢的部署方式:
|
60 |
+
|
61 |
+
#### 部署到 Vercel
|
62 |
+
|
63 |
+
[](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)
|
64 |
+
|
65 |
+
#### 本地运行
|
66 |
+
|
67 |
+
```bash
|
68 |
+
git clone https://github.com/gcui-art/suno-api.git
|
69 |
+
cd suno-api
|
70 |
+
npm install
|
71 |
+
```
|
72 |
+
|
73 |
+
或者,你也可以使用 [Docker Compose](https://docs.docker.com/compose/)
|
74 |
+
|
75 |
+
```bash
|
76 |
+
docker compose build && docker compose up
|
77 |
+
```
|
78 |
+
|
79 |
+
### 3. 配置 suno-api
|
80 |
+
|
81 |
+
- 如果部署到了 Vercel,请在 Vercel 后台,添加环境变量 `SUNO_COOKIE`,值为第一步获取的 cookie。
|
82 |
+
- 如果在本地运行,请在 .env 文件中添加:
|
83 |
+
|
84 |
+
```bash
|
85 |
+
SUNO_COOKIE=<your-cookie>
|
86 |
+
```
|
87 |
+
|
88 |
+
### 4. 运行 suno api
|
89 |
+
|
90 |
+
- 如果部署到了 Vercel:
|
91 |
+
- 请在 Vercel 后台,点击 `Deploy`,等待部署成功。
|
92 |
+
- 访问 `https://<vercel分配的域名>/api/get_limit` API 进行测试
|
93 |
+
- 如果在本地运行:
|
94 |
+
- 请运行 `npm run dev`
|
95 |
+
- 访问 `http://localhost:3000/api/get_limit` API 进行测试
|
96 |
+
- 如果返回以下结果:
|
97 |
+
|
98 |
+
```json
|
99 |
+
{
|
100 |
+
"credits_left": 0,
|
101 |
+
"period": "string",
|
102 |
+
"monthly_limit": 0,
|
103 |
+
"monthly_usage": 0
|
104 |
+
}
|
105 |
+
```
|
106 |
+
|
107 |
+
则已经正常运行。
|
108 |
+
|
109 |
+
### 5. 使用 Suno API
|
110 |
+
|
111 |
+
你可以在 [suno.gcui.ai](https://suno.gcui.ai/docs)查看详细的 API 文档,并在线测试。
|
112 |
+
|
113 |
+
## API 说明
|
114 |
+
|
115 |
+
Suno API 目前主要实现了以下 API:
|
116 |
+
|
117 |
+
```bash
|
118 |
+
- `/api/generate`: 创建音乐
|
119 |
+
- `/v1/chat/completions`: 创建音乐 - 用OpenAI API 兼容的格式调用 generate API
|
120 |
+
- `/api/custom_generate`: 创建音乐(自定义模式,支持设置歌词、音乐风格、设置标题等)
|
121 |
+
- `/api/generate_lyrics`: 根据Prompt创建歌词
|
122 |
+
- `/api/get`: 根据id获取音乐信息。获取多个请用","分隔,不传ids则返回所有音乐
|
123 |
+
- `/api/get_limit`: 获取配额信息
|
124 |
+
- `/api/extend_audio`: 在一首音乐的基础上,扩展音乐长度
|
125 |
+
- `/api/clip`: 检索特定音乐的信息
|
126 |
+
- `/api/concat`: 合并音乐,将扩展后的音乐和原始音乐合并
|
127 |
+
```
|
128 |
+
|
129 |
+
详细文档请查看演示站点:
|
130 |
+
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
131 |
+
|
132 |
+
## API 集成代码示例
|
133 |
+
|
134 |
+
### Python
|
135 |
+
|
136 |
+
```python
|
137 |
+
import time
|
138 |
+
import requests
|
139 |
+
|
140 |
+
# replace your vercel domain
|
141 |
+
base_url = 'http://localhost:3000'
|
142 |
+
|
143 |
+
|
144 |
+
def custom_generate_audio(payload):
|
145 |
+
url = f"{base_url}/api/custom_generate"
|
146 |
+
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
|
147 |
+
return response.json()
|
148 |
+
|
149 |
+
def extend_audio(payload):
|
150 |
+
url = f"{base_url}/api/extend_audio"
|
151 |
+
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
|
152 |
+
return response.json()
|
153 |
+
|
154 |
+
|
155 |
+
def generate_audio_by_prompt(payload):
|
156 |
+
url = f"{base_url}/api/generate"
|
157 |
+
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
|
158 |
+
return response.json()
|
159 |
+
|
160 |
+
|
161 |
+
def get_audio_information(audio_ids):
|
162 |
+
url = f"{base_url}/api/get?ids={audio_ids}"
|
163 |
+
response = requests.get(url)
|
164 |
+
return response.json()
|
165 |
+
|
166 |
+
|
167 |
+
def get_quota_information():
|
168 |
+
url = f"{base_url}/api/get_limit"
|
169 |
+
response = requests.get(url)
|
170 |
+
return response.json()
|
171 |
+
|
172 |
+
|
173 |
+
if __name__ == '__main__':
|
174 |
+
data = generate_audio_by_prompt({
|
175 |
+
"prompt": "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.",
|
176 |
+
"make_instrumental": False,
|
177 |
+
"wait_audio": False
|
178 |
+
})
|
179 |
+
|
180 |
+
ids = f"{data[0]['id']},{data[1]['id']}"
|
181 |
+
print(f"ids: {ids}")
|
182 |
+
|
183 |
+
for _ in range(60):
|
184 |
+
data = get_audio_information(ids)
|
185 |
+
if data[0]["status"] == 'streaming':
|
186 |
+
print(f"{data[0]['id']} ==> {data[0]['audio_url']}")
|
187 |
+
print(f"{data[1]['id']} ==> {data[1]['audio_url']}")
|
188 |
+
break
|
189 |
+
# sleep 5s
|
190 |
+
time.sleep(5)
|
191 |
+
|
192 |
+
```
|
193 |
+
|
194 |
+
### Js
|
195 |
+
|
196 |
+
```js
|
197 |
+
const axios = require("axios");
|
198 |
+
|
199 |
+
// replace your vercel domain
|
200 |
+
const baseUrl = "http://localhost:3000";
|
201 |
+
|
202 |
+
async function customGenerateAudio(payload) {
|
203 |
+
const url = `${baseUrl}/api/custom_generate`;
|
204 |
+
const response = await axios.post(url, payload, {
|
205 |
+
headers: { "Content-Type": "application/json" },
|
206 |
+
});
|
207 |
+
return response.data;
|
208 |
+
}
|
209 |
+
|
210 |
+
async function generateAudioByPrompt(payload) {
|
211 |
+
const url = `${baseUrl}/api/generate`;
|
212 |
+
const response = await axios.post(url, payload, {
|
213 |
+
headers: { "Content-Type": "application/json" },
|
214 |
+
});
|
215 |
+
return response.data;
|
216 |
+
}
|
217 |
+
async function extendAudio(payload) {
|
218 |
+
const url = `${baseUrl}/api/extend_audio`;
|
219 |
+
const response = await axios.post(url, payload, {
|
220 |
+
headers: { "Content-Type": "application/json" },
|
221 |
+
});
|
222 |
+
return response.data;
|
223 |
+
}
|
224 |
+
|
225 |
+
async function getAudioInformation(audioIds) {
|
226 |
+
const url = `${baseUrl}/api/get?ids=${audioIds}`;
|
227 |
+
const response = await axios.get(url);
|
228 |
+
return response.data;
|
229 |
+
}
|
230 |
+
|
231 |
+
async function getQuotaInformation() {
|
232 |
+
const url = `${baseUrl}/api/get_limit`;
|
233 |
+
const response = await axios.get(url);
|
234 |
+
return response.data;
|
235 |
+
}
|
236 |
+
|
237 |
+
async function main() {
|
238 |
+
const data = await generateAudioByPrompt({
|
239 |
+
prompt:
|
240 |
+
"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.",
|
241 |
+
make_instrumental: false,
|
242 |
+
wait_audio: false,
|
243 |
+
});
|
244 |
+
|
245 |
+
const ids = `${data[0].id},${data[1].id}`;
|
246 |
+
console.log(`ids: ${ids}`);
|
247 |
+
|
248 |
+
for (let i = 0; i < 60; i++) {
|
249 |
+
const data = await getAudioInformation(ids);
|
250 |
+
if (data[0].status === "streaming") {
|
251 |
+
console.log(`${data[0].id} ==> ${data[0].audio_url}`);
|
252 |
+
console.log(`${data[1].id} ==> ${data[1].audio_url}`);
|
253 |
+
break;
|
254 |
+
}
|
255 |
+
// sleep 5s
|
256 |
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
257 |
+
}
|
258 |
+
}
|
259 |
+
|
260 |
+
main();
|
261 |
+
```
|
262 |
+
|
263 |
+
## 集成到到常见的自定义 Agent 中
|
264 |
+
|
265 |
+
你可以把 suno ai 当做一个 工具/插件/Action 集成到你的 AI Agent 中。
|
266 |
+
|
267 |
+
### 集成到 GPTs
|
268 |
+
|
269 |
+
[coming soon...]
|
270 |
+
|
271 |
+
### 集成到 coze
|
272 |
+
|
273 |
+
[coming soon...]
|
274 |
+
|
275 |
+
### 集成到 LangChain
|
276 |
+
|
277 |
+
[coming soon...]
|
278 |
+
|
279 |
+
## 贡献指南
|
280 |
+
|
281 |
+
您有四种方式支持本项目:
|
282 |
+
|
283 |
+
1. Fork 项目并提交 PR:我们欢迎任何让这个组件和Editor变的更好的PR。
|
284 |
+
2. 提交Issue:我们欢迎任何合理的建议、bug反馈。
|
285 |
+
3. 捐赠:在项目的顶部我们放置了 Sponsor 按钮,如果这个项目帮助到了您,你可以请我们喝一杯,干杯☕。
|
286 |
+
4. 推荐:向其他人推荐本项目;点击Star;使用本项目后放置外链。
|
287 |
+
|
288 |
+
## 许可证
|
289 |
+
|
290 |
+
LGPL-3.0 或更高版本
|
291 |
+
|
292 |
+
## 你有一个问题/建议/困难/Bug?
|
293 |
+
|
294 |
+
我们使用Github的Issue来管理这些反馈,你可以提交一个。我们会经常来处理。
|
295 |
+
|
296 |
+
## 相关链接
|
297 |
+
|
298 |
+
- 项目仓库: [github.com/gcui-art/suno-api](https://github.com/gcui-art/suno-api)
|
299 |
+
- Suno.ai 官网: [suno.ai](https://suno.ai)
|
300 |
+
- 演示站点: [suno.gcui.ai](https://suno.gcui.ai)
|
301 |
+
|
302 |
+
## 声明
|
303 |
+
|
304 |
+
suno-api 是一个非官方的开源项目,仅供学习和研究使用。
|
docker-compose.yml
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: '3'
|
2 |
+
|
3 |
+
services:
|
4 |
+
suno-api:
|
5 |
+
build:
|
6 |
+
context: .
|
7 |
+
args:
|
8 |
+
SUNO_COOKIE: ${SUNO_COOKIE}
|
9 |
+
volumes:
|
10 |
+
- ./public:/app/public
|
11 |
+
ports:
|
12 |
+
- "3000:3000"
|
icon.png
ADDED
![]() |
next.config.mjs
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('next').NextConfig} */
|
2 |
+
const nextConfig = {};
|
3 |
+
|
4 |
+
export default nextConfig;
|
package.json
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "suno-api",
|
3 |
+
"description": "Use API to call the music generation service of suno.ai, and easily integrate it into agents like GPTs.",
|
4 |
+
"author": {
|
5 |
+
"name": "gcui.ai",
|
6 |
+
"url": "https://github.com/gcui-art/"
|
7 |
+
},
|
8 |
+
"license": "LGPL-3.0-or-later",
|
9 |
+
"version": "1.1.0",
|
10 |
+
"private": true,
|
11 |
+
"scripts": {
|
12 |
+
"dev": "next dev",
|
13 |
+
"build": "next build",
|
14 |
+
"start": "next start",
|
15 |
+
"lint": "next lint"
|
16 |
+
},
|
17 |
+
"dependencies": {
|
18 |
+
"@vercel/analytics": "^1.2.2",
|
19 |
+
"axios": "^1.6.8",
|
20 |
+
"axios-cookiejar-support": "^5.0.0",
|
21 |
+
"next": "14.1.4",
|
22 |
+
"next-swagger-doc": "^0.4.0",
|
23 |
+
"pino": "^8.19.0",
|
24 |
+
"pino-pretty": "^11.0.0",
|
25 |
+
"react": "^18",
|
26 |
+
"react-dom": "^18",
|
27 |
+
"react-markdown": "^9.0.1",
|
28 |
+
"swagger-ui-react": "^5.12.3",
|
29 |
+
"tough-cookie": "^4.1.4",
|
30 |
+
"user-agents": "^1.1.156"
|
31 |
+
},
|
32 |
+
"devDependencies": {
|
33 |
+
"@tailwindcss/typography": "^0.5.12",
|
34 |
+
"@types/node": "^20",
|
35 |
+
"@types/react": "^18",
|
36 |
+
"@types/react-dom": "^18",
|
37 |
+
"@types/swagger-ui-react": "^4.18.3",
|
38 |
+
"@types/tough-cookie": "^4.0.5",
|
39 |
+
"@types/user-agents": "^1.0.4",
|
40 |
+
"autoprefixer": "^10.0.1",
|
41 |
+
"eslint": "^8.57.0",
|
42 |
+
"eslint-config-next": "14.1.4",
|
43 |
+
"postcss": "^8",
|
44 |
+
"tailwindcss": "^3.3.0",
|
45 |
+
"typescript": "^5"
|
46 |
+
}
|
47 |
+
}
|
pnpm-lock.yaml
ADDED
The diff for this file is too large to render.
See raw diff
|
|
postcss.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
plugins: {
|
3 |
+
tailwindcss: {},
|
4 |
+
autoprefixer: {},
|
5 |
+
},
|
6 |
+
};
|
public/get-cookie-demo.gif
ADDED
![]() |
Git LFS Details
|
public/get-cookie-demo.mp4
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:6ee1df7a41fdc5064253da0d13c69abb4af879a05b593f30157894e44ed84472
|
3 |
+
size 6762147
|
public/github-logo.webp
ADDED
![]() |
public/github-mark.png
ADDED
![]() |
public/next.svg
ADDED
|
public/suno-banner.png
ADDED
![]() |
public/swagger-suno-api.json
ADDED
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"openapi": "3.0.3",
|
3 |
+
"info": {
|
4 |
+
"title": "suno-api",
|
5 |
+
"description": "",
|
6 |
+
"version": "",
|
7 |
+
"license": { "name": "gcui-art", "url": "https://github.com/gcui-art/" }
|
8 |
+
},
|
9 |
+
"tags": [{ "name": "\u9ed8\u8ba4\u5206\u7ec4" }],
|
10 |
+
"paths": {
|
11 |
+
"/api/custom_generate": {
|
12 |
+
"post": {
|
13 |
+
"summary": "Generate Audio - Custom Mode",
|
14 |
+
"description": "The custom mode enables users to provide additional details about the music, such as music genre, lyrics, and more.2 audio files will be generated for each request, consuming a total of 10 credits.wait_audio can be set to API mode:\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.\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.",
|
15 |
+
"tags": ["\u9ed8\u8ba4\u5206\u7ec4"],
|
16 |
+
"requestBody": {
|
17 |
+
"content": {
|
18 |
+
"application/json": {
|
19 |
+
"schema": {
|
20 |
+
"type": "object",
|
21 |
+
"required": ["prompt", "tags", "title"],
|
22 |
+
"properties": {
|
23 |
+
"prompt": {
|
24 |
+
"type": "string",
|
25 |
+
"description": "Detailed prompt, including information such as music lyrics.",
|
26 |
+
"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"
|
27 |
+
},
|
28 |
+
"tags": {
|
29 |
+
"type": "string",
|
30 |
+
"description": "Music genre",
|
31 |
+
"example": "pop metal male melancholic"
|
32 |
+
},
|
33 |
+
"title": {
|
34 |
+
"type": "string",
|
35 |
+
"description": "Music title",
|
36 |
+
"example": "Silent Battlefield"
|
37 |
+
},
|
38 |
+
"make_instrumental": {
|
39 |
+
"type": "boolean",
|
40 |
+
"description": "Whether to generate instrumental music",
|
41 |
+
"example": "false"
|
42 |
+
},
|
43 |
+
"model": {
|
44 |
+
"type": "string",
|
45 |
+
"description": "Model name ,default is chirp-v3-5",
|
46 |
+
"example": "chirp-v3-5|chirp-v3-0"
|
47 |
+
},
|
48 |
+
"wait_audio": {
|
49 |
+
"type": "boolean",
|
50 |
+
"description": "Whether to wait for music generation, default is false, directly return audio task information; set to true, will wait for up to 100s until the audio is generated.",
|
51 |
+
"example": "false"
|
52 |
+
}
|
53 |
+
}
|
54 |
+
}
|
55 |
+
}
|
56 |
+
}
|
57 |
+
},
|
58 |
+
"responses": {
|
59 |
+
"200": {
|
60 |
+
"description": "\u6210\u529f",
|
61 |
+
"content": {
|
62 |
+
"application/json": {
|
63 |
+
"schema": {
|
64 |
+
"type": "array",
|
65 |
+
"items": {
|
66 |
+
"type": "object",
|
67 |
+
"required": ["0", "1"],
|
68 |
+
"properties": [
|
69 |
+
{ "$ref": "#/components/schemas/audio info" },
|
70 |
+
{ "$ref": "#/components/schemas/audio info" }
|
71 |
+
]
|
72 |
+
}
|
73 |
+
}
|
74 |
+
}
|
75 |
+
}
|
76 |
+
}
|
77 |
+
}
|
78 |
+
}
|
79 |
+
},
|
80 |
+
"/api/generate": {
|
81 |
+
"post": {
|
82 |
+
"summary": "Generate audio based on Prompt.",
|
83 |
+
"description": "It will automatically fill in the lyrics.2 audio files will be generated for each request, consuming a total of 10 credits.wait_audio can be set to API mode:\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.\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.",
|
84 |
+
"tags": ["\u9ed8\u8ba4\u5206\u7ec4"],
|
85 |
+
"requestBody": {
|
86 |
+
"content": {
|
87 |
+
"application/json": {
|
88 |
+
"schema": {
|
89 |
+
"type": "object",
|
90 |
+
"required": ["prompt", "make_instrumental", "wait_audio"],
|
91 |
+
"properties": {
|
92 |
+
"prompt": {
|
93 |
+
"type": "string",
|
94 |
+
"description": "Prompt",
|
95 |
+
"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."
|
96 |
+
},
|
97 |
+
"make_instrumental": {
|
98 |
+
"type": "boolean",
|
99 |
+
"description": "Whether to generate instrumental music",
|
100 |
+
"example": "false"
|
101 |
+
},
|
102 |
+
"model": {
|
103 |
+
"type": "string",
|
104 |
+
"description": "Model name ,default is chirp-v3-5",
|
105 |
+
"example": "chirp-v3-5|chirp-v3-0"
|
106 |
+
},
|
107 |
+
"wait_audio": {
|
108 |
+
"type": "boolean",
|
109 |
+
"description": "Whether to wait for music generation, default is false, directly return audio task information; set to true, will wait for up to 100s until the audio is generated.",
|
110 |
+
"example": "false"
|
111 |
+
}
|
112 |
+
}
|
113 |
+
}
|
114 |
+
}
|
115 |
+
}
|
116 |
+
},
|
117 |
+
"responses": {
|
118 |
+
"200": {
|
119 |
+
"description": "\u6210\u529f",
|
120 |
+
"content": {
|
121 |
+
"application/json": {
|
122 |
+
"schema": {
|
123 |
+
"type": "array",
|
124 |
+
"items": {
|
125 |
+
"type": "object",
|
126 |
+
"required": ["0", "1"],
|
127 |
+
"properties": [
|
128 |
+
{ "$ref": "#/components/schemas/audio info" },
|
129 |
+
{ "$ref": "#/components/schemas/audio info" }
|
130 |
+
]
|
131 |
+
}
|
132 |
+
}
|
133 |
+
}
|
134 |
+
}
|
135 |
+
}
|
136 |
+
}
|
137 |
+
}
|
138 |
+
},
|
139 |
+
"/api/get": {
|
140 |
+
"get": {
|
141 |
+
"summary": "Get audio information",
|
142 |
+
"description": "",
|
143 |
+
"tags": ["\u9ed8\u8ba4\u5206\u7ec4"],
|
144 |
+
"parameters": [
|
145 |
+
{
|
146 |
+
"in": "query",
|
147 |
+
"name": "ids",
|
148 |
+
"description": "Audio IDs, separated by commas.",
|
149 |
+
"required": true,
|
150 |
+
"schema": { "type": "string" }
|
151 |
+
}
|
152 |
+
],
|
153 |
+
"responses": { "200": { "description": "\u6210\u529f" } }
|
154 |
+
}
|
155 |
+
},
|
156 |
+
"/api/get_limit": {
|
157 |
+
"get": {
|
158 |
+
"summary": "Get quota information.",
|
159 |
+
"description": "",
|
160 |
+
"tags": ["\u9ed8\u8ba4\u5206\u7ec4"],
|
161 |
+
"responses": {
|
162 |
+
"200": {
|
163 |
+
"description": "\u6210\u529f",
|
164 |
+
"content": {
|
165 |
+
"application/json": {
|
166 |
+
"schema": {
|
167 |
+
"type": "object",
|
168 |
+
"required": [
|
169 |
+
"credits_left",
|
170 |
+
"period",
|
171 |
+
"monthly_limit",
|
172 |
+
"monthly_usage"
|
173 |
+
],
|
174 |
+
"properties": {
|
175 |
+
"credits_left": {
|
176 |
+
"type": "number",
|
177 |
+
"description": "Remaining credits,Each generated audio consumes 5 credits."
|
178 |
+
},
|
179 |
+
"period": { "type": "string", "description": "Period" },
|
180 |
+
"monthly_limit": {
|
181 |
+
"type": "number",
|
182 |
+
"description": "Monthly limit"
|
183 |
+
},
|
184 |
+
"monthly_usage": {
|
185 |
+
"type": "number",
|
186 |
+
"description": "Monthly usage"
|
187 |
+
}
|
188 |
+
}
|
189 |
+
}
|
190 |
+
}
|
191 |
+
}
|
192 |
+
}
|
193 |
+
}
|
194 |
+
}
|
195 |
+
}
|
196 |
+
},
|
197 |
+
"components": {
|
198 |
+
"schemas": {
|
199 |
+
"audio info": {
|
200 |
+
"type": "object",
|
201 |
+
"required": [
|
202 |
+
"id",
|
203 |
+
"title",
|
204 |
+
"image_url",
|
205 |
+
"lyric",
|
206 |
+
"audio_url",
|
207 |
+
"video_url",
|
208 |
+
"created_at",
|
209 |
+
"model_name",
|
210 |
+
"status",
|
211 |
+
"gpt_description_prompt",
|
212 |
+
"prompt",
|
213 |
+
"type",
|
214 |
+
"tags"
|
215 |
+
],
|
216 |
+
"properties": {
|
217 |
+
"id": { "type": "string", "description": "audio id" },
|
218 |
+
"title": { "type": "string", "description": "music title" },
|
219 |
+
"image_url": { "type": "string", "description": "music cover image" },
|
220 |
+
"lyric": { "type": "string", "description": "music lyric" },
|
221 |
+
"audio_url": {
|
222 |
+
"type": "string",
|
223 |
+
"description": "music download url"
|
224 |
+
},
|
225 |
+
"video_url": {
|
226 |
+
"type": "string",
|
227 |
+
"description": "Music video download link, can be used to share"
|
228 |
+
},
|
229 |
+
"created_at": { "type": "string", "description": "Create time" },
|
230 |
+
"model_name": {
|
231 |
+
"type": "string",
|
232 |
+
"description": "suno model name, chirp-v3"
|
233 |
+
},
|
234 |
+
"status": {
|
235 |
+
"type": "string",
|
236 |
+
"description": "The generated states include submitted, queue, streaming, complete."
|
237 |
+
},
|
238 |
+
"gpt_description_prompt": {
|
239 |
+
"type": "string",
|
240 |
+
"description": "Simple mode on user input prompt, Suno will generate formal prompts, lyrics, etc."
|
241 |
+
},
|
242 |
+
"prompt": {
|
243 |
+
"type": "string",
|
244 |
+
"description": "The final prompt for executing the generation task, customized by the user in custom mode, automatically generated by Suno in simple mode."
|
245 |
+
},
|
246 |
+
"type": { "type": "string", "description": "Type" },
|
247 |
+
"tags": {
|
248 |
+
"type": "string",
|
249 |
+
"description": "Music genre. User-provided in custom mode, automatically generated by Suno in simple mode."
|
250 |
+
}
|
251 |
+
},
|
252 |
+
"title": "audio info",
|
253 |
+
"description": "audio info"
|
254 |
+
}
|
255 |
+
}
|
256 |
+
}
|
257 |
+
}
|
public/vercel.svg
ADDED
|
src/app/api/clip/route.ts
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse, NextRequest } from "next/server";
|
2 |
+
import { sunoApi } from "@/lib/SunoApi";
|
3 |
+
import { corsHeaders } from "@/lib/utils";
|
4 |
+
|
5 |
+
export const dynamic = "force-dynamic";
|
6 |
+
|
7 |
+
export async function GET(req: NextRequest) {
|
8 |
+
if (req.method === 'GET') {
|
9 |
+
try {
|
10 |
+
const url = new URL(req.url);
|
11 |
+
const clipId = url.searchParams.get('id');
|
12 |
+
if (clipId == null) {
|
13 |
+
return new NextResponse(JSON.stringify({ error: 'Missing parameter id' }), {
|
14 |
+
status: 400,
|
15 |
+
headers: {
|
16 |
+
'Content-Type': 'application/json',
|
17 |
+
...corsHeaders
|
18 |
+
}
|
19 |
+
});
|
20 |
+
}
|
21 |
+
|
22 |
+
const audioInfo = await (await sunoApi).getClip(clipId);
|
23 |
+
|
24 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
25 |
+
status: 200,
|
26 |
+
headers: {
|
27 |
+
'Content-Type': 'application/json',
|
28 |
+
...corsHeaders
|
29 |
+
}
|
30 |
+
});
|
31 |
+
} catch (error) {
|
32 |
+
console.error('Error fetching audio:', error);
|
33 |
+
|
34 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
35 |
+
status: 500,
|
36 |
+
headers: {
|
37 |
+
'Content-Type': 'application/json',
|
38 |
+
...corsHeaders
|
39 |
+
}
|
40 |
+
});
|
41 |
+
}
|
42 |
+
} else {
|
43 |
+
return new NextResponse('Method Not Allowed', {
|
44 |
+
headers: {
|
45 |
+
Allow: 'GET',
|
46 |
+
...corsHeaders
|
47 |
+
},
|
48 |
+
status: 405
|
49 |
+
});
|
50 |
+
}
|
51 |
+
}
|
52 |
+
|
53 |
+
export async function OPTIONS(request: Request) {
|
54 |
+
return new Response(null, {
|
55 |
+
status: 200,
|
56 |
+
headers: corsHeaders
|
57 |
+
});
|
58 |
+
}
|
src/app/api/concat/route.ts
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse, NextRequest } from "next/server";
|
2 |
+
import { sunoApi } from "@/lib/SunoApi";
|
3 |
+
import { corsHeaders } from "@/lib/utils";
|
4 |
+
|
5 |
+
export const dynamic = "force-dynamic";
|
6 |
+
|
7 |
+
export async function POST(req: NextRequest) {
|
8 |
+
if (req.method === 'POST') {
|
9 |
+
try {
|
10 |
+
const body = await req.json();
|
11 |
+
const { clip_id } = body;
|
12 |
+
if (!clip_id) {
|
13 |
+
return new NextResponse(JSON.stringify({ error: 'Clip id is required' }), {
|
14 |
+
status: 400,
|
15 |
+
headers: {
|
16 |
+
'Content-Type': 'application/json',
|
17 |
+
...corsHeaders
|
18 |
+
}
|
19 |
+
});
|
20 |
+
}
|
21 |
+
const audioInfo = await (await sunoApi).concatenate(clip_id);
|
22 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
23 |
+
status: 200,
|
24 |
+
headers: {
|
25 |
+
'Content-Type': 'application/json',
|
26 |
+
...corsHeaders
|
27 |
+
}
|
28 |
+
});
|
29 |
+
} catch (error: any) {
|
30 |
+
console.error('Error generating concatenating audio:', error.response.data);
|
31 |
+
if (error.response.status === 402) {
|
32 |
+
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
33 |
+
status: 402,
|
34 |
+
headers: {
|
35 |
+
'Content-Type': 'application/json',
|
36 |
+
...corsHeaders
|
37 |
+
}
|
38 |
+
});
|
39 |
+
}
|
40 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
41 |
+
status: 500,
|
42 |
+
headers: {
|
43 |
+
'Content-Type': 'application/json',
|
44 |
+
...corsHeaders
|
45 |
+
}
|
46 |
+
});
|
47 |
+
}
|
48 |
+
} else {
|
49 |
+
return new NextResponse('Method Not Allowed', {
|
50 |
+
headers: {
|
51 |
+
Allow: 'POST',
|
52 |
+
...corsHeaders
|
53 |
+
},
|
54 |
+
status: 405
|
55 |
+
});
|
56 |
+
}
|
57 |
+
}
|
58 |
+
|
59 |
+
export async function OPTIONS(request: Request) {
|
60 |
+
return new Response(null, {
|
61 |
+
status: 200,
|
62 |
+
headers: corsHeaders
|
63 |
+
});
|
64 |
+
}
|
src/app/api/custom_generate/route.ts
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse, NextRequest } from "next/server";
|
2 |
+
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
3 |
+
import { corsHeaders } from "@/lib/utils";
|
4 |
+
|
5 |
+
export const maxDuration = 60; // allow longer timeout for wait_audio == true
|
6 |
+
export const dynamic = "force-dynamic";
|
7 |
+
|
8 |
+
export async function POST(req: NextRequest) {
|
9 |
+
if (req.method === 'POST') {
|
10 |
+
try {
|
11 |
+
const body = await req.json();
|
12 |
+
const { prompt, tags, title, make_instrumental, model, wait_audio } = body;
|
13 |
+
const audioInfo = await (await sunoApi).custom_generate(
|
14 |
+
prompt, tags, title,
|
15 |
+
Boolean(make_instrumental),
|
16 |
+
model || DEFAULT_MODEL,
|
17 |
+
Boolean(wait_audio)
|
18 |
+
);
|
19 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
20 |
+
status: 200,
|
21 |
+
headers: {
|
22 |
+
'Content-Type': 'application/json',
|
23 |
+
...corsHeaders
|
24 |
+
}
|
25 |
+
});
|
26 |
+
} catch (error: any) {
|
27 |
+
console.error('Error generating custom audio:', error.response.data);
|
28 |
+
if (error.response.status === 402) {
|
29 |
+
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
30 |
+
status: 402,
|
31 |
+
headers: {
|
32 |
+
'Content-Type': 'application/json',
|
33 |
+
...corsHeaders
|
34 |
+
}
|
35 |
+
});
|
36 |
+
}
|
37 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
38 |
+
status: 500,
|
39 |
+
headers: {
|
40 |
+
'Content-Type': 'application/json',
|
41 |
+
...corsHeaders
|
42 |
+
}
|
43 |
+
});
|
44 |
+
}
|
45 |
+
} else {
|
46 |
+
return new NextResponse('Method Not Allowed', {
|
47 |
+
headers: {
|
48 |
+
Allow: 'POST',
|
49 |
+
...corsHeaders
|
50 |
+
},
|
51 |
+
status: 405
|
52 |
+
});
|
53 |
+
}
|
54 |
+
}
|
55 |
+
|
56 |
+
export async function OPTIONS(request: Request) {
|
57 |
+
return new Response(null, {
|
58 |
+
status: 200,
|
59 |
+
headers: corsHeaders
|
60 |
+
});
|
61 |
+
}
|
src/app/api/extend_audio/route.ts
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse, NextRequest } from "next/server";
|
2 |
+
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
3 |
+
import { corsHeaders } from "@/lib/utils";
|
4 |
+
|
5 |
+
export const dynamic = "force-dynamic";
|
6 |
+
|
7 |
+
export async function POST(req: NextRequest) {
|
8 |
+
if (req.method === 'POST') {
|
9 |
+
try {
|
10 |
+
const body = await req.json();
|
11 |
+
const { audio_id, prompt, continue_at, tags, title, model } = body;
|
12 |
+
|
13 |
+
if (!audio_id) {
|
14 |
+
return new NextResponse(JSON.stringify({ error: 'Audio ID is required' }), {
|
15 |
+
status: 400,
|
16 |
+
headers: {
|
17 |
+
'Content-Type': 'application/json',
|
18 |
+
...corsHeaders
|
19 |
+
}
|
20 |
+
});
|
21 |
+
}
|
22 |
+
|
23 |
+
const audioInfo = await (await sunoApi)
|
24 |
+
.extendAudio(audio_id, prompt, continue_at, tags, title, model || DEFAULT_MODEL);
|
25 |
+
|
26 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
27 |
+
status: 200,
|
28 |
+
headers: {
|
29 |
+
'Content-Type': 'application/json',
|
30 |
+
...corsHeaders
|
31 |
+
}
|
32 |
+
});
|
33 |
+
} catch (error: any) {
|
34 |
+
console.error('Error extend audio:', JSON.stringify(error.response.data));
|
35 |
+
if (error.response.status === 402) {
|
36 |
+
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
37 |
+
status: 402,
|
38 |
+
headers: {
|
39 |
+
'Content-Type': 'application/json',
|
40 |
+
...corsHeaders
|
41 |
+
}
|
42 |
+
});
|
43 |
+
}
|
44 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
45 |
+
status: 500,
|
46 |
+
headers: {
|
47 |
+
'Content-Type': 'application/json',
|
48 |
+
...corsHeaders
|
49 |
+
}
|
50 |
+
});
|
51 |
+
}
|
52 |
+
} else {
|
53 |
+
return new NextResponse('Method Not Allowed', {
|
54 |
+
headers: {
|
55 |
+
Allow: 'POST',
|
56 |
+
...corsHeaders
|
57 |
+
},
|
58 |
+
status: 405
|
59 |
+
});
|
60 |
+
}
|
61 |
+
}
|
62 |
+
|
63 |
+
|
64 |
+
export async function OPTIONS(request: Request) {
|
65 |
+
return new Response(null, {
|
66 |
+
status: 200,
|
67 |
+
headers: corsHeaders
|
68 |
+
});
|
69 |
+
}
|
src/app/api/generate/route.ts
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse, NextRequest } from "next/server";
|
2 |
+
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
3 |
+
import { corsHeaders } from "@/lib/utils";
|
4 |
+
|
5 |
+
export const dynamic = "force-dynamic";
|
6 |
+
|
7 |
+
export async function POST(req: NextRequest) {
|
8 |
+
if (req.method === 'POST') {
|
9 |
+
try {
|
10 |
+
const body = await req.json();
|
11 |
+
const { prompt, make_instrumental, model, wait_audio } = body;
|
12 |
+
|
13 |
+
const audioInfo = await (await sunoApi).generate(
|
14 |
+
prompt,
|
15 |
+
Boolean(make_instrumental),
|
16 |
+
model || DEFAULT_MODEL,
|
17 |
+
Boolean(wait_audio)
|
18 |
+
);
|
19 |
+
|
20 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
21 |
+
status: 200,
|
22 |
+
headers: {
|
23 |
+
'Content-Type': 'application/json',
|
24 |
+
...corsHeaders
|
25 |
+
}
|
26 |
+
});
|
27 |
+
} catch (error: any) {
|
28 |
+
console.error('Error generating custom audio:', JSON.stringify(error.response.data));
|
29 |
+
if (error.response.status === 402) {
|
30 |
+
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
31 |
+
status: 402,
|
32 |
+
headers: {
|
33 |
+
'Content-Type': 'application/json',
|
34 |
+
...corsHeaders
|
35 |
+
}
|
36 |
+
});
|
37 |
+
}
|
38 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
39 |
+
status: 500,
|
40 |
+
headers: {
|
41 |
+
'Content-Type': 'application/json',
|
42 |
+
...corsHeaders
|
43 |
+
}
|
44 |
+
});
|
45 |
+
}
|
46 |
+
} else {
|
47 |
+
return new NextResponse('Method Not Allowed', {
|
48 |
+
headers: {
|
49 |
+
Allow: 'POST',
|
50 |
+
...corsHeaders
|
51 |
+
},
|
52 |
+
status: 405
|
53 |
+
});
|
54 |
+
}
|
55 |
+
}
|
56 |
+
|
57 |
+
|
58 |
+
export async function OPTIONS(request: Request) {
|
59 |
+
return new Response(null, {
|
60 |
+
status: 200,
|
61 |
+
headers: corsHeaders
|
62 |
+
});
|
63 |
+
}
|
src/app/api/generate_lyrics/route.ts
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse, NextRequest } from "next/server";
|
2 |
+
import { sunoApi } from "@/lib/SunoApi";
|
3 |
+
import { corsHeaders } from "@/lib/utils";
|
4 |
+
|
5 |
+
export const dynamic = "force-dynamic";
|
6 |
+
|
7 |
+
export async function POST(req: NextRequest) {
|
8 |
+
if (req.method === 'POST') {
|
9 |
+
try {
|
10 |
+
const body = await req.json();
|
11 |
+
const { prompt } = body;
|
12 |
+
|
13 |
+
const lyrics = await (await sunoApi).generateLyrics(prompt);
|
14 |
+
|
15 |
+
return new NextResponse(JSON.stringify(lyrics), {
|
16 |
+
status: 200,
|
17 |
+
headers: {
|
18 |
+
'Content-Type': 'application/json',
|
19 |
+
...corsHeaders
|
20 |
+
}
|
21 |
+
});
|
22 |
+
} catch (error: any) {
|
23 |
+
console.error('Error generating lyrics:', JSON.stringify(error.response.data));
|
24 |
+
if (error.response.status === 402) {
|
25 |
+
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
26 |
+
status: 402,
|
27 |
+
headers: {
|
28 |
+
'Content-Type': 'application/json',
|
29 |
+
...corsHeaders
|
30 |
+
}
|
31 |
+
});
|
32 |
+
}
|
33 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
34 |
+
status: 500,
|
35 |
+
headers: {
|
36 |
+
'Content-Type': 'application/json',
|
37 |
+
...corsHeaders
|
38 |
+
}
|
39 |
+
});
|
40 |
+
}
|
41 |
+
} else {
|
42 |
+
return new NextResponse('Method Not Allowed', {
|
43 |
+
headers: {
|
44 |
+
Allow: 'POST',
|
45 |
+
...corsHeaders
|
46 |
+
},
|
47 |
+
status: 405
|
48 |
+
});
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
export async function OPTIONS(request: Request) {
|
53 |
+
return new Response(null, {
|
54 |
+
status: 200,
|
55 |
+
headers: corsHeaders
|
56 |
+
});
|
57 |
+
}
|
src/app/api/get/route.ts
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse, NextRequest } from "next/server";
|
2 |
+
import { sunoApi } from "@/lib/SunoApi";
|
3 |
+
import { corsHeaders } from "@/lib/utils";
|
4 |
+
|
5 |
+
export const dynamic = "force-dynamic";
|
6 |
+
|
7 |
+
export async function GET(req: NextRequest) {
|
8 |
+
if (req.method === 'GET') {
|
9 |
+
try {
|
10 |
+
const url = new URL(req.url);
|
11 |
+
const songIds = url.searchParams.get('ids');
|
12 |
+
let audioInfo = [];
|
13 |
+
if (songIds && songIds.length > 0) {
|
14 |
+
const idsArray = songIds.split(',');
|
15 |
+
audioInfo = await (await sunoApi).get(idsArray);
|
16 |
+
} else {
|
17 |
+
audioInfo = await (await sunoApi).get();
|
18 |
+
}
|
19 |
+
|
20 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
21 |
+
status: 200,
|
22 |
+
headers: {
|
23 |
+
'Content-Type': 'application/json',
|
24 |
+
...corsHeaders
|
25 |
+
}
|
26 |
+
});
|
27 |
+
} catch (error) {
|
28 |
+
console.error('Error fetching audio:', error);
|
29 |
+
|
30 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
31 |
+
status: 500,
|
32 |
+
headers: {
|
33 |
+
'Content-Type': 'application/json',
|
34 |
+
...corsHeaders
|
35 |
+
}
|
36 |
+
});
|
37 |
+
}
|
38 |
+
} else {
|
39 |
+
return new NextResponse('Method Not Allowed', {
|
40 |
+
headers: {
|
41 |
+
Allow: 'GET',
|
42 |
+
...corsHeaders
|
43 |
+
},
|
44 |
+
status: 405
|
45 |
+
});
|
46 |
+
}
|
47 |
+
}
|
48 |
+
|
49 |
+
export async function OPTIONS(request: Request) {
|
50 |
+
return new Response(null, {
|
51 |
+
status: 200,
|
52 |
+
headers: corsHeaders
|
53 |
+
});
|
54 |
+
}
|
src/app/api/get_limit/route.ts
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse, NextRequest } from "next/server";
|
2 |
+
import { sunoApi } from "@/lib/SunoApi";
|
3 |
+
import { corsHeaders } from "@/lib/utils";
|
4 |
+
|
5 |
+
export const dynamic = "force-dynamic";
|
6 |
+
|
7 |
+
export async function GET(req: NextRequest) {
|
8 |
+
if (req.method === 'GET') {
|
9 |
+
try {
|
10 |
+
|
11 |
+
const limit = await (await sunoApi).get_credits();
|
12 |
+
|
13 |
+
|
14 |
+
return new NextResponse(JSON.stringify(limit), {
|
15 |
+
status: 200,
|
16 |
+
headers: {
|
17 |
+
'Content-Type': 'application/json',
|
18 |
+
...corsHeaders
|
19 |
+
}
|
20 |
+
});
|
21 |
+
} catch (error) {
|
22 |
+
console.error('Error fetching limit:', error);
|
23 |
+
|
24 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error. ' + error }), {
|
25 |
+
status: 500,
|
26 |
+
headers: {
|
27 |
+
'Content-Type': 'application/json',
|
28 |
+
...corsHeaders
|
29 |
+
}
|
30 |
+
});
|
31 |
+
}
|
32 |
+
} else {
|
33 |
+
return new NextResponse('Method Not Allowed', {
|
34 |
+
headers: {
|
35 |
+
Allow: 'GET',
|
36 |
+
...corsHeaders
|
37 |
+
},
|
38 |
+
status: 405
|
39 |
+
});
|
40 |
+
}
|
41 |
+
}
|
42 |
+
|
43 |
+
export async function OPTIONS(request: Request) {
|
44 |
+
return new Response(null, {
|
45 |
+
status: 200,
|
46 |
+
headers: corsHeaders
|
47 |
+
});
|
48 |
+
}
|
src/app/components/Footer.tsx
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Link from "next/link";
|
2 |
+
import Image from "next/image";
|
3 |
+
|
4 |
+
export default function Footer() {
|
5 |
+
return (
|
6 |
+
<footer className=" flex w-full justify-center py-4 items-center
|
7 |
+
bg-indigo-900 text-white/60 backdrop-blur-2xl font-mono text-sm px-4 lg:px-0
|
8 |
+
">
|
9 |
+
<p className="px-6 py-3 rounded-full flex justify-center items-center gap-2
|
10 |
+
hover:text-white duration-200
|
11 |
+
">
|
12 |
+
|
13 |
+
</p>
|
14 |
+
<p className="px-6 py-3 rounded-full flex justify-center items-center gap-2
|
15 |
+
hover:text-white duration-200
|
16 |
+
">
|
17 |
+
<span>© 2024</span>
|
18 |
+
<Link href="https://github.com/gcui-art/suno-api/">
|
19 |
+
gcui-art/suno-api
|
20 |
+
</Link>
|
21 |
+
</p>
|
22 |
+
</footer>
|
23 |
+
);
|
24 |
+
}
|
src/app/components/Header.tsx
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Link from "next/link";
|
2 |
+
import Image from "next/image";
|
3 |
+
import Logo from "./Logo";
|
4 |
+
|
5 |
+
export default function Header() {
|
6 |
+
return (
|
7 |
+
<nav className=" flex w-full justify-center py-4 items-center
|
8 |
+
border-b border-gray-300 backdrop-blur-2xl font-mono text-sm px-4 lg:px-0">
|
9 |
+
<div className="max-w-3xl flex w-full items-center justify-between">
|
10 |
+
<div className="font-medium text-xl text-indigo-900 flex items-center gap-2">
|
11 |
+
<Logo className="w-4 h-4" />
|
12 |
+
<Link href='/'>
|
13 |
+
Suno API
|
14 |
+
</Link>
|
15 |
+
</div>
|
16 |
+
<div className="flex items-center justify-center gap-1 text-sm font-light text-indigo-900/90">
|
17 |
+
<p className="p-2 lg:px-6 lg:py-3 rounded-full flex justify-center items-center
|
18 |
+
lg:hover:bg-indigo-300 duration-200
|
19 |
+
">
|
20 |
+
<Link href="/">
|
21 |
+
Get Started
|
22 |
+
</Link>
|
23 |
+
</p>
|
24 |
+
<p className="p-2 lg:px-6 lg:py-3 rounded-full flex justify-center items-center
|
25 |
+
lg:hover:bg-indigo-300 duration-200
|
26 |
+
">
|
27 |
+
<Link href="/docs">
|
28 |
+
API Docs
|
29 |
+
</Link>
|
30 |
+
</p>
|
31 |
+
<p className="p-2 lg:px-6 lg:py-3 rounded-full flex justify-center items-center
|
32 |
+
lg:hover:bg-indigo-300 duration-200
|
33 |
+
">
|
34 |
+
<a href="https://github.com/gcui-art/suno-api/"
|
35 |
+
target="_blank"
|
36 |
+
className="flex items-center justify-center gap-1">
|
37 |
+
<span className="">
|
38 |
+
<Image src="/github-mark.png" alt="GitHub Logo" width={20} height={20} />
|
39 |
+
</span>
|
40 |
+
<span>Github</span>
|
41 |
+
</a>
|
42 |
+
</p>
|
43 |
+
</div>
|
44 |
+
|
45 |
+
|
46 |
+
|
47 |
+
</div>
|
48 |
+
</nav>
|
49 |
+
);
|
50 |
+
}
|
src/app/components/Logo.tsx
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
|
3 |
+
export default function Logo({ className = '', ...props }) {
|
4 |
+
return (
|
5 |
+
<span className=" bg-indigo-900 rounded-full p-2">
|
6 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className={className}
|
7 |
+
fill="none" stroke="#ffffff" strokeWidth="1"
|
8 |
+
strokeLinecap="round" strokeLinejoin="round">
|
9 |
+
<path d="M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3" />
|
10 |
+
</svg>
|
11 |
+
</span>
|
12 |
+
);
|
13 |
+
}
|
src/app/components/Section.tsx
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
/**
|
3 |
+
*
|
4 |
+
* @param param0
|
5 |
+
* @returns
|
6 |
+
*/
|
7 |
+
export default function Section({
|
8 |
+
children,
|
9 |
+
className
|
10 |
+
}: {
|
11 |
+
children?: React.ReactNode | string,
|
12 |
+
className?: string
|
13 |
+
}) {
|
14 |
+
|
15 |
+
return (
|
16 |
+
<section className={`mx-auto w-full px-4 lg:px-0 ${className}`} >
|
17 |
+
<div className=" max-w-3xl mx-auto">
|
18 |
+
{children}
|
19 |
+
</div>
|
20 |
+
</section>
|
21 |
+
);
|
22 |
+
};
|
src/app/components/Swagger.tsx
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
import 'swagger-ui-react/swagger-ui.css';
|
3 |
+
import dynamic from "next/dynamic";
|
4 |
+
|
5 |
+
type Props = {
|
6 |
+
spec: Record<string, any>,
|
7 |
+
};
|
8 |
+
|
9 |
+
const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false });
|
10 |
+
|
11 |
+
function Swagger({ spec }: Props) {
|
12 |
+
return <SwaggerUI spec={spec}/>;
|
13 |
+
}
|
14 |
+
|
15 |
+
export default Swagger;
|
src/app/docs/page.tsx
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import Swagger from '../components/Swagger';
|
3 |
+
import spec from './swagger-suno-api.json'; // 直接导入JSON文件
|
4 |
+
import Section from '../components/Section';
|
5 |
+
import Markdown from 'react-markdown';
|
6 |
+
|
7 |
+
|
8 |
+
export default function Docs() {
|
9 |
+
return (
|
10 |
+
<>
|
11 |
+
<Section className="my-10">
|
12 |
+
<article className="prose lg:prose-lg max-w-3xl pt-10">
|
13 |
+
<h1 className=' text-center text-indigo-900'>
|
14 |
+
API Docs
|
15 |
+
</h1>
|
16 |
+
<Markdown>
|
17 |
+
{`
|
18 |
+
---
|
19 |
+
\`gcui-art/suno-api\` currently mainly implements the following APIs:
|
20 |
+
|
21 |
+
\`\`\`bash
|
22 |
+
- \`/api/generate\`: Generate music
|
23 |
+
- \`/v1/chat/completions\`: Generate music - Call the generate API in a format
|
24 |
+
that works with OpenAI’s API.
|
25 |
+
- \`/api/custom_generate\`: Generate music (Custom Mode, support setting lyrics,
|
26 |
+
music style, title, etc.)
|
27 |
+
- \`/api/generate_lyrics\`: Generate lyrics based on prompt
|
28 |
+
- \`/api/get\`: Get music information based on the id. Use “,” to separate multiple
|
29 |
+
ids. If no IDs are provided, all music will be returned.
|
30 |
+
- \`/api/get_limit\`: Get quota Info
|
31 |
+
- \`/api/extend_audio\`: Extend audio length
|
32 |
+
- \`/api/clip\`: Get clip information based on ID passed as query parameter \`id\`
|
33 |
+
- \`/api/concat\`: Generate the whole song from extensions
|
34 |
+
\`\`\`
|
35 |
+
|
36 |
+
Feel free to explore the detailed API parameters and conduct tests on this page.
|
37 |
+
`}
|
38 |
+
</Markdown>
|
39 |
+
</article>
|
40 |
+
</Section>
|
41 |
+
<Section className="my-10">
|
42 |
+
<article className='prose lg:prose-lg max-w-3xl py-10'>
|
43 |
+
<h2 className='text-center'>
|
44 |
+
Details of the API and testing it online
|
45 |
+
</h2>
|
46 |
+
<p className='text-red-800 italic'>
|
47 |
+
This is just a demo, bound to a test account. Please do not use it frequently, so that more people can test online.
|
48 |
+
</p>
|
49 |
+
</article>
|
50 |
+
|
51 |
+
<div className=' border p-4 rounded-2xl shadow-xl hover:shadow-none duration-200'>
|
52 |
+
<Swagger spec={spec} />
|
53 |
+
</div>
|
54 |
+
|
55 |
+
</Section>
|
56 |
+
</>
|
57 |
+
|
58 |
+
);
|
59 |
+
}
|
src/app/docs/swagger-suno-api.json
ADDED
@@ -0,0 +1,600 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"openapi": "3.0.3",
|
3 |
+
"info": {
|
4 |
+
"title": "suno-api",
|
5 |
+
"description": "Use API to call the music generation service of Suno.ai and easily integrate it into agents like GPTs.",
|
6 |
+
"version": "1.1.0"
|
7 |
+
},
|
8 |
+
"tags": [
|
9 |
+
{
|
10 |
+
"name": "default"
|
11 |
+
}
|
12 |
+
],
|
13 |
+
"paths": {
|
14 |
+
"/api/generate": {
|
15 |
+
"post": {
|
16 |
+
"summary": "Generate audio based on Prompt.",
|
17 |
+
"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.",
|
18 |
+
"tags": ["default"],
|
19 |
+
"requestBody": {
|
20 |
+
"content": {
|
21 |
+
"application/json": {
|
22 |
+
"schema": {
|
23 |
+
"type": "object",
|
24 |
+
"required": ["prompt", "make_instrumental", "wait_audio"],
|
25 |
+
"properties": {
|
26 |
+
"prompt": {
|
27 |
+
"type": "string",
|
28 |
+
"description": "Prompt",
|
29 |
+
"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."
|
30 |
+
},
|
31 |
+
"make_instrumental": {
|
32 |
+
"type": "boolean",
|
33 |
+
"description": "Whether to generate instrumental music",
|
34 |
+
"example": "false"
|
35 |
+
},
|
36 |
+
"model": {
|
37 |
+
"type": "string",
|
38 |
+
"description": "Model name ,default is chirp-v3-5",
|
39 |
+
"example": "chirp-v3-5|chirp-v3-0"
|
40 |
+
},
|
41 |
+
"wait_audio": {
|
42 |
+
"type": "boolean",
|
43 |
+
"description": "Whether to wait for music generation, default is false, directly return audio task information; set to true, will wait for up to 100s until the audio is generated.",
|
44 |
+
"example": "false"
|
45 |
+
}
|
46 |
+
}
|
47 |
+
}
|
48 |
+
}
|
49 |
+
}
|
50 |
+
},
|
51 |
+
"responses": {
|
52 |
+
"200": {
|
53 |
+
"description": "success",
|
54 |
+
"content": {
|
55 |
+
"application/json": {
|
56 |
+
"schema": {
|
57 |
+
"type": "array",
|
58 |
+
"items": {
|
59 |
+
"type": "object",
|
60 |
+
"required": ["0", "1"],
|
61 |
+
"properties": [
|
62 |
+
{
|
63 |
+
"$ref": "#/components/schemas/audio_info"
|
64 |
+
},
|
65 |
+
{
|
66 |
+
"$ref": "#/components/schemas/audio_info"
|
67 |
+
}
|
68 |
+
]
|
69 |
+
}
|
70 |
+
}
|
71 |
+
}
|
72 |
+
}
|
73 |
+
}
|
74 |
+
}
|
75 |
+
}
|
76 |
+
},
|
77 |
+
"/v1/chat/completions": {
|
78 |
+
"post": {
|
79 |
+
"summary": "Generate audio based on Prompt - OpenAI API format compatibility.",
|
80 |
+
"description": "Convert the `/api/generate` API to be compatible with the OpenAI `/v1/chat/completions` API format. \n\nGenerally used in OpenAI compatible clients.",
|
81 |
+
"tags": ["default"],
|
82 |
+
"requestBody": {
|
83 |
+
"content": {
|
84 |
+
"application/json": {
|
85 |
+
"schema": {
|
86 |
+
"type": "object",
|
87 |
+
"required": ["prompt"],
|
88 |
+
"properties": {
|
89 |
+
"prompt": {
|
90 |
+
"type": "string",
|
91 |
+
"description": "Prompt",
|
92 |
+
"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."
|
93 |
+
}
|
94 |
+
}
|
95 |
+
}
|
96 |
+
}
|
97 |
+
}
|
98 |
+
},
|
99 |
+
"responses": {
|
100 |
+
"200": {
|
101 |
+
"description": "success",
|
102 |
+
"content": {
|
103 |
+
"application/json": {
|
104 |
+
"schema": {
|
105 |
+
"type": "object",
|
106 |
+
"properties": {
|
107 |
+
"data": {
|
108 |
+
"type": "string",
|
109 |
+
"description": "Text description for music, with details like title, album cover, lyrics, and more."
|
110 |
+
}
|
111 |
+
}
|
112 |
+
}
|
113 |
+
}
|
114 |
+
}
|
115 |
+
}
|
116 |
+
}
|
117 |
+
}
|
118 |
+
},
|
119 |
+
"/api/custom_generate": {
|
120 |
+
"post": {
|
121 |
+
"summary": "Generate Audio - Custom Mode",
|
122 |
+
"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.",
|
123 |
+
"tags": ["default"],
|
124 |
+
"requestBody": {
|
125 |
+
"content": {
|
126 |
+
"application/json": {
|
127 |
+
"schema": {
|
128 |
+
"type": "object",
|
129 |
+
"required": ["prompt", "tags", "title"],
|
130 |
+
"properties": {
|
131 |
+
"prompt": {
|
132 |
+
"type": "string",
|
133 |
+
"description": "Detailed prompt, including information such as music lyrics.",
|
134 |
+
"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"
|
135 |
+
},
|
136 |
+
"tags": {
|
137 |
+
"type": "string",
|
138 |
+
"description": "Music genre",
|
139 |
+
"example": "pop metal male melancholic"
|
140 |
+
},
|
141 |
+
"title": {
|
142 |
+
"type": "string",
|
143 |
+
"description": "Music title",
|
144 |
+
"example": "Silent Battlefield"
|
145 |
+
},
|
146 |
+
"make_instrumental": {
|
147 |
+
"type": "boolean",
|
148 |
+
"description": "Whether to generate instrumental music",
|
149 |
+
"example": "false"
|
150 |
+
},
|
151 |
+
"model": {
|
152 |
+
"type": "string",
|
153 |
+
"description": "Model name ,default is chirp-v3-5",
|
154 |
+
"example": "chirp-v3-5|chirp-v3-0"
|
155 |
+
},
|
156 |
+
"wait_audio": {
|
157 |
+
"type": "boolean",
|
158 |
+
"description": "Whether to wait for music generation, default is false, directly return audio task information; set to true, will wait for up to 100s until the audio is generated.",
|
159 |
+
"example": "false"
|
160 |
+
}
|
161 |
+
}
|
162 |
+
}
|
163 |
+
}
|
164 |
+
}
|
165 |
+
},
|
166 |
+
"responses": {
|
167 |
+
"200": {
|
168 |
+
"description": "success",
|
169 |
+
"content": {
|
170 |
+
"application/json": {
|
171 |
+
"schema": {
|
172 |
+
"type": "array",
|
173 |
+
"items": {
|
174 |
+
"type": "object",
|
175 |
+
"required": ["0", "1"],
|
176 |
+
"properties": [
|
177 |
+
{
|
178 |
+
"$ref": "#/components/schemas/audio_info"
|
179 |
+
},
|
180 |
+
{
|
181 |
+
"$ref": "#/components/schemas/audio_info"
|
182 |
+
}
|
183 |
+
]
|
184 |
+
}
|
185 |
+
}
|
186 |
+
}
|
187 |
+
}
|
188 |
+
}
|
189 |
+
}
|
190 |
+
}
|
191 |
+
},
|
192 |
+
"/api/extend_audio": {
|
193 |
+
"post": {
|
194 |
+
"summary": "Extend audio length.",
|
195 |
+
"description": "Extend audio length.",
|
196 |
+
"tags": ["default"],
|
197 |
+
"requestBody": {
|
198 |
+
"content": {
|
199 |
+
"application/json": {
|
200 |
+
"schema": {
|
201 |
+
"type": "object",
|
202 |
+
"required": ["audio_id"],
|
203 |
+
"properties": {
|
204 |
+
"audio_id": {
|
205 |
+
"type": "string",
|
206 |
+
"description": "The ID of the audio clip to extend.",
|
207 |
+
"example": "e76498dc-6ab4-4a10-a19f-8a095790e28d"
|
208 |
+
},
|
209 |
+
"prompt": {
|
210 |
+
"type": "string",
|
211 |
+
"description": "Detailed prompt, including information such as music lyrics.",
|
212 |
+
"example": "[lrc]Silent 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[endlrc]"
|
213 |
+
},
|
214 |
+
"continue_at": {
|
215 |
+
"type": "string",
|
216 |
+
"description": "Extend a new clip from a song at mm:ss(e.g. 00:30). Default extends from the end of the song.",
|
217 |
+
"example": "109.96"
|
218 |
+
},
|
219 |
+
"title": {
|
220 |
+
"type": "string",
|
221 |
+
"description": "Music title",
|
222 |
+
"example": ""
|
223 |
+
},
|
224 |
+
"tags": {
|
225 |
+
"type": "string",
|
226 |
+
"description": "Music genre",
|
227 |
+
"example": ""
|
228 |
+
},
|
229 |
+
"model": {
|
230 |
+
"type": "string",
|
231 |
+
"description": "Model name ,default is chirp-v3-5",
|
232 |
+
"example": "chirp-v3-5|chirp-v3-0"
|
233 |
+
}
|
234 |
+
}
|
235 |
+
}
|
236 |
+
}
|
237 |
+
}
|
238 |
+
}
|
239 |
+
}
|
240 |
+
},
|
241 |
+
"/api/generate_lyrics": {
|
242 |
+
"post": {
|
243 |
+
"summary": "Generate lyrics based on Prompt.",
|
244 |
+
"description": "Generate lyrics based on Prompt.",
|
245 |
+
"tags": ["default"],
|
246 |
+
"requestBody": {
|
247 |
+
"content": {
|
248 |
+
"application/json": {
|
249 |
+
"schema": {
|
250 |
+
"type": "object",
|
251 |
+
"required": ["prompt"],
|
252 |
+
"properties": {
|
253 |
+
"prompt": {
|
254 |
+
"type": "string",
|
255 |
+
"description": "Prompt",
|
256 |
+
"example": "A soothing lullaby"
|
257 |
+
}
|
258 |
+
}
|
259 |
+
}
|
260 |
+
}
|
261 |
+
}
|
262 |
+
},
|
263 |
+
"responses": {
|
264 |
+
"200": {
|
265 |
+
"description": "success",
|
266 |
+
"content": {
|
267 |
+
"application/json": {
|
268 |
+
"schema": {
|
269 |
+
"type": "object",
|
270 |
+
"properties": {
|
271 |
+
"text": {
|
272 |
+
"type": "string",
|
273 |
+
"description": "Lyrics"
|
274 |
+
},
|
275 |
+
"title": {
|
276 |
+
"type": "string",
|
277 |
+
"description": "music title"
|
278 |
+
},
|
279 |
+
"status": {
|
280 |
+
"type": "string",
|
281 |
+
"description": "Status"
|
282 |
+
}
|
283 |
+
}
|
284 |
+
}
|
285 |
+
}
|
286 |
+
}
|
287 |
+
}
|
288 |
+
}
|
289 |
+
}
|
290 |
+
},
|
291 |
+
"/api/get": {
|
292 |
+
"get": {
|
293 |
+
"summary": "Get audio information",
|
294 |
+
"description": "",
|
295 |
+
"tags": ["default"],
|
296 |
+
"parameters": [
|
297 |
+
{
|
298 |
+
"in": "query",
|
299 |
+
"name": "ids",
|
300 |
+
"description": "Audio IDs, separated by commas. Leave blank to return a list of all music.",
|
301 |
+
"required": false,
|
302 |
+
"schema": {
|
303 |
+
"type": "string"
|
304 |
+
}
|
305 |
+
}
|
306 |
+
],
|
307 |
+
"responses": {
|
308 |
+
"200": {
|
309 |
+
"description": "success",
|
310 |
+
"content": {
|
311 |
+
"application/json": {
|
312 |
+
"schema": {
|
313 |
+
"type": "array",
|
314 |
+
"items": {
|
315 |
+
"type": "object",
|
316 |
+
"required": ["0", "1"],
|
317 |
+
"properties": [
|
318 |
+
{
|
319 |
+
"$ref": "#/components/schemas/audio_info"
|
320 |
+
},
|
321 |
+
{
|
322 |
+
"$ref": "#/components/schemas/audio_info"
|
323 |
+
}
|
324 |
+
]
|
325 |
+
}
|
326 |
+
}
|
327 |
+
}
|
328 |
+
}
|
329 |
+
}
|
330 |
+
}
|
331 |
+
}
|
332 |
+
},
|
333 |
+
"/api/get_limit": {
|
334 |
+
"get": {
|
335 |
+
"summary": "Get quota information.",
|
336 |
+
"description": "",
|
337 |
+
"tags": ["default"],
|
338 |
+
"responses": {
|
339 |
+
"200": {
|
340 |
+
"description": "success",
|
341 |
+
"content": {
|
342 |
+
"application/json": {
|
343 |
+
"schema": {
|
344 |
+
"type": "object",
|
345 |
+
"required": [
|
346 |
+
"credits_left",
|
347 |
+
"period",
|
348 |
+
"monthly_limit",
|
349 |
+
"monthly_usage"
|
350 |
+
],
|
351 |
+
"properties": {
|
352 |
+
"credits_left": {
|
353 |
+
"type": "number",
|
354 |
+
"description": "Remaining credits,Each generated audio consumes 5 credits."
|
355 |
+
},
|
356 |
+
"period": {
|
357 |
+
"type": "string",
|
358 |
+
"description": "Period"
|
359 |
+
},
|
360 |
+
"monthly_limit": {
|
361 |
+
"type": "number",
|
362 |
+
"description": "Monthly limit"
|
363 |
+
},
|
364 |
+
"monthly_usage": {
|
365 |
+
"type": "number",
|
366 |
+
"description": "Monthly usage"
|
367 |
+
}
|
368 |
+
}
|
369 |
+
}
|
370 |
+
}
|
371 |
+
}
|
372 |
+
}
|
373 |
+
}
|
374 |
+
}
|
375 |
+
},
|
376 |
+
"/api/clip": {
|
377 |
+
"get": {
|
378 |
+
"summary": "Get clip information based on ID.",
|
379 |
+
"description": "Retrieve specific clip information using the provided clip ID as a query parameter.",
|
380 |
+
"tags": ["default"],
|
381 |
+
"parameters": [
|
382 |
+
{
|
383 |
+
"name": "id",
|
384 |
+
"in": "query",
|
385 |
+
"required": true,
|
386 |
+
"description": "Clip ID",
|
387 |
+
"schema": {
|
388 |
+
"type": "string"
|
389 |
+
}
|
390 |
+
}
|
391 |
+
],
|
392 |
+
"responses": {
|
393 |
+
"200": {
|
394 |
+
"description": "success",
|
395 |
+
"content": {
|
396 |
+
"application/json": {
|
397 |
+
"schema": {
|
398 |
+
"$ref": "#/components/schemas/audio_info"
|
399 |
+
}
|
400 |
+
}
|
401 |
+
}
|
402 |
+
},
|
403 |
+
"400": {
|
404 |
+
"description": "Missing parameter id",
|
405 |
+
"content": {
|
406 |
+
"application/json": {
|
407 |
+
"schema": {
|
408 |
+
"type": "object",
|
409 |
+
"properties": {
|
410 |
+
"error": {
|
411 |
+
"type": "string",
|
412 |
+
"example": "Missing parameter id"
|
413 |
+
}
|
414 |
+
}
|
415 |
+
}
|
416 |
+
}
|
417 |
+
}
|
418 |
+
},
|
419 |
+
"500": {
|
420 |
+
"description": "Internal server error",
|
421 |
+
"content": {
|
422 |
+
"application/json": {
|
423 |
+
"schema": {
|
424 |
+
"type": "object",
|
425 |
+
"properties": {
|
426 |
+
"error": {
|
427 |
+
"type": "string",
|
428 |
+
"example": "Internal server error"
|
429 |
+
}
|
430 |
+
}
|
431 |
+
}
|
432 |
+
}
|
433 |
+
}
|
434 |
+
}
|
435 |
+
}
|
436 |
+
}
|
437 |
+
},
|
438 |
+
"/api/concat": {
|
439 |
+
"post": {
|
440 |
+
"summary": "Generate the whole song from extensions.",
|
441 |
+
"description": "Concatenate audio clips to generate a complete song using the provided clip ID.",
|
442 |
+
"tags": ["default"],
|
443 |
+
"requestBody": {
|
444 |
+
"content": {
|
445 |
+
"application/json": {
|
446 |
+
"schema": {
|
447 |
+
"type": "object",
|
448 |
+
"required": ["clip_id"],
|
449 |
+
"properties": {
|
450 |
+
"clip_id": {
|
451 |
+
"type": "string",
|
452 |
+
"description": "Clip ID"
|
453 |
+
}
|
454 |
+
}
|
455 |
+
}
|
456 |
+
}
|
457 |
+
}
|
458 |
+
},
|
459 |
+
"responses": {
|
460 |
+
"200": {
|
461 |
+
"description": "success",
|
462 |
+
"content": {
|
463 |
+
"application/json": {
|
464 |
+
"schema": {
|
465 |
+
"$ref": "#/components/schemas/audio_info"
|
466 |
+
}
|
467 |
+
}
|
468 |
+
}
|
469 |
+
},
|
470 |
+
"400": {
|
471 |
+
"description": "Clip id is required",
|
472 |
+
"content": {
|
473 |
+
"application/json": {
|
474 |
+
"schema": {
|
475 |
+
"type": "object",
|
476 |
+
"properties": {
|
477 |
+
"error": {
|
478 |
+
"type": "string",
|
479 |
+
"example": "Clip id is required"
|
480 |
+
}
|
481 |
+
}
|
482 |
+
}
|
483 |
+
}
|
484 |
+
}
|
485 |
+
},
|
486 |
+
"402": {
|
487 |
+
"description": "Payment required",
|
488 |
+
"content": {
|
489 |
+
"application/json": {
|
490 |
+
"schema": {
|
491 |
+
"type": "object",
|
492 |
+
"properties": {
|
493 |
+
"error": {
|
494 |
+
"type": "string",
|
495 |
+
"example": "Payment required"
|
496 |
+
}
|
497 |
+
}
|
498 |
+
}
|
499 |
+
}
|
500 |
+
}
|
501 |
+
},
|
502 |
+
"500": {
|
503 |
+
"description": "Internal server error",
|
504 |
+
"content": {
|
505 |
+
"application/json": {
|
506 |
+
"schema": {
|
507 |
+
"type": "object",
|
508 |
+
"properties": {
|
509 |
+
"error": {
|
510 |
+
"type": "string",
|
511 |
+
"example": "Internal server error"
|
512 |
+
}
|
513 |
+
}
|
514 |
+
}
|
515 |
+
}
|
516 |
+
}
|
517 |
+
}
|
518 |
+
}
|
519 |
+
}
|
520 |
+
}
|
521 |
+
},
|
522 |
+
"components": {
|
523 |
+
"schemas": {
|
524 |
+
"audio_info": {
|
525 |
+
"type": "object",
|
526 |
+
"required": [
|
527 |
+
"id",
|
528 |
+
"title",
|
529 |
+
"image_url",
|
530 |
+
"lyric",
|
531 |
+
"audio_url",
|
532 |
+
"video_url",
|
533 |
+
"created_at",
|
534 |
+
"model_name",
|
535 |
+
"status",
|
536 |
+
"gpt_description_prompt",
|
537 |
+
"prompt",
|
538 |
+
"type",
|
539 |
+
"tags"
|
540 |
+
],
|
541 |
+
"properties": {
|
542 |
+
"id": {
|
543 |
+
"type": "string",
|
544 |
+
"description": "audio id"
|
545 |
+
},
|
546 |
+
"title": {
|
547 |
+
"type": "string",
|
548 |
+
"description": "music title"
|
549 |
+
},
|
550 |
+
"image_url": {
|
551 |
+
"type": "string",
|
552 |
+
"description": "music cover image"
|
553 |
+
},
|
554 |
+
"lyric": {
|
555 |
+
"type": "string",
|
556 |
+
"description": "music lyric"
|
557 |
+
},
|
558 |
+
"audio_url": {
|
559 |
+
"type": "string",
|
560 |
+
"description": "music download url"
|
561 |
+
},
|
562 |
+
"video_url": {
|
563 |
+
"type": "string",
|
564 |
+
"description": "Music video download link, can be used to share"
|
565 |
+
},
|
566 |
+
"created_at": {
|
567 |
+
"type": "string",
|
568 |
+
"description": "Create time"
|
569 |
+
},
|
570 |
+
"model_name": {
|
571 |
+
"type": "string",
|
572 |
+
"description": "suno model name, chirp-v3"
|
573 |
+
},
|
574 |
+
"status": {
|
575 |
+
"type": "string",
|
576 |
+
"description": "The generated states include submitted, queue, streaming, complete."
|
577 |
+
},
|
578 |
+
"gpt_description_prompt": {
|
579 |
+
"type": "string",
|
580 |
+
"description": "Simple mode on user input prompt, Suno will generate formal prompts, lyrics, etc."
|
581 |
+
},
|
582 |
+
"prompt": {
|
583 |
+
"type": "string",
|
584 |
+
"description": "The final prompt for executing the generation task, customized by the user in custom mode, automatically generated by Suno in simple mode."
|
585 |
+
},
|
586 |
+
"type": {
|
587 |
+
"type": "string",
|
588 |
+
"description": "Type"
|
589 |
+
},
|
590 |
+
"tags": {
|
591 |
+
"type": "string",
|
592 |
+
"description": "Music genre. User-provided in custom mode, automatically generated by Suno in simple mode."
|
593 |
+
}
|
594 |
+
},
|
595 |
+
"title": "audio_info",
|
596 |
+
"description": "Audio Info"
|
597 |
+
}
|
598 |
+
}
|
599 |
+
}
|
600 |
+
}
|
src/app/favicon.ico
ADDED
|
src/app/globals.css
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
4 |
+
|
5 |
+
:root {
|
6 |
+
--foreground-rgb: 0, 0, 0;
|
7 |
+
--background-start-rgb: 255, 255, 255;
|
8 |
+
--background-end-rgb: 255, 255, 255;
|
9 |
+
}
|
10 |
+
|
11 |
+
body {
|
12 |
+
color: rgb(var(--foreground-rgb));
|
13 |
+
background: linear-gradient(
|
14 |
+
to bottom,
|
15 |
+
transparent,
|
16 |
+
rgb(var(--background-end-rgb))
|
17 |
+
)
|
18 |
+
rgb(var(--background-start-rgb));
|
19 |
+
}
|
20 |
+
|
21 |
+
@layer utilities {
|
22 |
+
.text-balance {
|
23 |
+
text-wrap: balance;
|
24 |
+
}
|
25 |
+
}
|
src/app/layout.tsx
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Metadata } from "next";
|
2 |
+
import { Inter } from "next/font/google";
|
3 |
+
import "./globals.css";
|
4 |
+
import Header from "./components/Header";
|
5 |
+
import Footer from "./components/Footer";
|
6 |
+
import { Analytics } from "@vercel/analytics/react"
|
7 |
+
|
8 |
+
const inter = Inter({ subsets: ["latin"] });
|
9 |
+
|
10 |
+
export const metadata: Metadata = {
|
11 |
+
title: "suno api",
|
12 |
+
description: "Use API to call the music generation ai of suno.ai",
|
13 |
+
keywords: ["suno", "suno api", "suno.ai", "api", "music", "generation", "ai"],
|
14 |
+
creator: "@gcui.ai",
|
15 |
+
};
|
16 |
+
|
17 |
+
export default function RootLayout({
|
18 |
+
children,
|
19 |
+
}: Readonly<{
|
20 |
+
children: React.ReactNode;
|
21 |
+
}>) {
|
22 |
+
return (
|
23 |
+
<html lang="en">
|
24 |
+
<body className={`${inter.className} overflow-y-scroll`} >
|
25 |
+
<Header />
|
26 |
+
<main className="flex flex-col items-center m-auto w-full">
|
27 |
+
{children}
|
28 |
+
</main>
|
29 |
+
<Footer />
|
30 |
+
<Analytics />
|
31 |
+
</body>
|
32 |
+
</html>
|
33 |
+
);
|
34 |
+
}
|
src/app/page.tsx
ADDED
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Section from "./components/Section";
|
2 |
+
import Markdown from 'react-markdown';
|
3 |
+
|
4 |
+
|
5 |
+
export default function Home() {
|
6 |
+
|
7 |
+
const markdown = `
|
8 |
+
|
9 |
+
---
|
10 |
+
## 👋 Introduction
|
11 |
+
|
12 |
+
Suno.ai v3 is an amazing AI music service. Although the official API is not yet available, we couldn't wait to integrate its capabilities somewhere.
|
13 |
+
|
14 |
+
We discovered that some users have similar needs, so we decided to open-source this project, hoping you'll like it.
|
15 |
+
|
16 |
+
We update quickly, please star us on Github: [github.com/gcui-art/suno-api](https://github.com/gcui-art/suno-api) ⭐
|
17 |
+
|
18 |
+
## 🌟 Features
|
19 |
+
|
20 |
+
- Perfectly implements the creation API from \`app.suno.ai\`
|
21 |
+
- Compatible with the format of OpenAI’s \`/v1/chat/completions\` API.
|
22 |
+
- Automatically keep the account active.
|
23 |
+
- Supports \`Custom Mode\`
|
24 |
+
- One-click deployment to Vercel
|
25 |
+
- 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.
|
26 |
+
- Permissive open-source license, allowing you to freely integrate and modify.
|
27 |
+
|
28 |
+
## 🚀 Getting Started
|
29 |
+
|
30 |
+
### 1. Obtain the cookie of your app.suno.ai account
|
31 |
+
|
32 |
+
1. Head over to [app.suno.ai](https://app.suno.ai) using your browser.
|
33 |
+
2. Open up the browser console: hit \`F12\` or access the \`Developer Tools\`.
|
34 |
+
3. Navigate to the \`Network tab\`.
|
35 |
+
4. Give the page a quick refresh.
|
36 |
+
5. Identify the request that includes the keyword \`client?_clerk_js_version\`.
|
37 |
+
6. Click on it and switch over to the \`Header\` tab.
|
38 |
+
7. Locate the \`Cookie\` section, hover your mouse over it, and copy the value of the Cookie.
|
39 |
+
`;
|
40 |
+
|
41 |
+
|
42 |
+
const markdown_part2 = `
|
43 |
+
### 2. Clone and deploy this project
|
44 |
+
|
45 |
+
You can choose your preferred deployment method:
|
46 |
+
|
47 |
+
#### Deploy to Vercel
|
48 |
+
|
49 |
+
[](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)
|
50 |
+
|
51 |
+
#### Run locally
|
52 |
+
|
53 |
+
\`\`\`bash
|
54 |
+
git clone https://github.com/gcui-art/suno-api.git
|
55 |
+
cd suno-api
|
56 |
+
npm install
|
57 |
+
\`\`\`
|
58 |
+
|
59 |
+
### 3. Configure suno-api
|
60 |
+
|
61 |
+
- If deployed to Vercel, please add an environment variable \`SUNO_COOKIE\` in the Vercel dashboard, with the value of the cookie obtained in the first step.
|
62 |
+
|
63 |
+
- If you’re running this locally, be sure to add the following to your \`.env\` file:
|
64 |
+
|
65 |
+
\`\`\`bash
|
66 |
+
SUNO_COOKIE=<your-cookie>
|
67 |
+
\`\`\`
|
68 |
+
|
69 |
+
### 4. Run suno-api
|
70 |
+
|
71 |
+
- If you’ve deployed to Vercel:
|
72 |
+
- Please click on Deploy in the Vercel dashboard and wait for the deployment to be successful.
|
73 |
+
- Visit the \`https://<vercel-assigned-domain>/api/get_limit\` API for testing.
|
74 |
+
- If running locally:
|
75 |
+
- Run \`npm run dev\`.
|
76 |
+
- Visit the \`http://localhost:3000/api/get_limit\` API for testing.
|
77 |
+
- If the following result is returned:
|
78 |
+
|
79 |
+
\`\`\`json
|
80 |
+
{
|
81 |
+
"credits_left": 50,
|
82 |
+
"period": "day",
|
83 |
+
"monthly_limit": 50,
|
84 |
+
"monthly_usage": 50
|
85 |
+
}
|
86 |
+
\`\`\`
|
87 |
+
|
88 |
+
it means the program is running normally.
|
89 |
+
|
90 |
+
### 5. Use Suno API
|
91 |
+
|
92 |
+
You can check out the detailed API documentation at [suno.gcui.ai/docs](https://suno.gcui.ai/docs).
|
93 |
+
|
94 |
+
## 📚 API Reference
|
95 |
+
|
96 |
+
Suno API currently mainly implements the following APIs:
|
97 |
+
|
98 |
+
\`\`\`bash
|
99 |
+
- \`/api/generate\`: Generate music
|
100 |
+
- \`/v1/chat/completions\`: Generate music - Call the generate API in a format
|
101 |
+
that works with OpenAI’s API.
|
102 |
+
- \`/api/custom_generate\`: Generate music (Custom Mode, support setting lyrics,
|
103 |
+
music style, title, etc.)
|
104 |
+
- \`/api/generate_lyrics\`: Generate lyrics based on prompt
|
105 |
+
- \`/api/get\`: Get music list
|
106 |
+
- \`/api/get?ids=\`: Get music Info by id, separate multiple id with ",".
|
107 |
+
- \`/api/get_limit\`: Get quota Info
|
108 |
+
- \`/api/extend_audio\`: Extend audio length
|
109 |
+
- \`/api/concat\`: Generate the whole song from extensions
|
110 |
+
\`\`\`
|
111 |
+
|
112 |
+
For more detailed documentation, please check out the demo site:
|
113 |
+
|
114 |
+
👉 [suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
115 |
+
|
116 |
+
`;
|
117 |
+
return (
|
118 |
+
<>
|
119 |
+
<Section className="">
|
120 |
+
<div className="flex flex-col m-auto py-20 text-center items-center justify-center gap-4 my-8
|
121 |
+
lg:px-20 px-4
|
122 |
+
bg-indigo-900/90 rounded-2xl border shadow-2xl hover:shadow-none duration-200">
|
123 |
+
<span className=" px-5 py-1 text-xs font-light border rounded-full
|
124 |
+
border-white/20 uppercase text-white/50">
|
125 |
+
Unofficial
|
126 |
+
</span>
|
127 |
+
<h1 className="font-bold text-7xl flex text-white/90">
|
128 |
+
Suno AI API
|
129 |
+
</h1>
|
130 |
+
<p className="text-white/80 text-lg">
|
131 |
+
`Suno-api` is an open-source project that enables you to set up your own Suno AI API.
|
132 |
+
</p>
|
133 |
+
</div>
|
134 |
+
|
135 |
+
</Section>
|
136 |
+
<Section className="my-10">
|
137 |
+
<article className="prose lg:prose-lg max-w-3xl">
|
138 |
+
<Markdown>
|
139 |
+
{markdown}
|
140 |
+
</Markdown>
|
141 |
+
<video controls width="1024" className="w-full border rounded-lg shadow-xl">
|
142 |
+
<source src="/get-cookie-demo.mp4" type="video/mp4" />
|
143 |
+
Your browser does not support frames.
|
144 |
+
</video>
|
145 |
+
<Markdown>
|
146 |
+
{markdown_part2}
|
147 |
+
</Markdown>
|
148 |
+
</article>
|
149 |
+
</Section>
|
150 |
+
|
151 |
+
|
152 |
+
</>
|
153 |
+
);
|
154 |
+
}
|
src/app/v1/chat/completions/route.ts
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse, NextRequest } from "next/server";
|
2 |
+
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
3 |
+
import { corsHeaders } from "@/lib/utils";
|
4 |
+
|
5 |
+
export const dynamic = "force-dynamic";
|
6 |
+
|
7 |
+
/**
|
8 |
+
* desc
|
9 |
+
*
|
10 |
+
*/
|
11 |
+
export async function POST(req: NextRequest) {
|
12 |
+
try {
|
13 |
+
|
14 |
+
const body = await req.json();
|
15 |
+
|
16 |
+
let userMessage = null;
|
17 |
+
const { messages } = body;
|
18 |
+
for (let message of messages) {
|
19 |
+
if (message.role == 'user') {
|
20 |
+
userMessage = message;
|
21 |
+
}
|
22 |
+
}
|
23 |
+
|
24 |
+
if (!userMessage) {
|
25 |
+
return new NextResponse(JSON.stringify({ error: 'Prompt message is required' }), {
|
26 |
+
status: 400,
|
27 |
+
headers: {
|
28 |
+
'Content-Type': 'application/json',
|
29 |
+
...corsHeaders
|
30 |
+
}
|
31 |
+
});
|
32 |
+
}
|
33 |
+
|
34 |
+
|
35 |
+
const audioInfo = await (await sunoApi).generate(userMessage.content, true, DEFAULT_MODEL, true);
|
36 |
+
|
37 |
+
const audio = audioInfo[0]
|
38 |
+
const data = `## Song Title: ${audio.title}\n\n### Lyrics:\n${audio.lyric}\n### Listen to the song: ${audio.audio_url}`
|
39 |
+
|
40 |
+
return new NextResponse(data, {
|
41 |
+
status: 200,
|
42 |
+
headers: corsHeaders
|
43 |
+
});
|
44 |
+
} catch (error: any) {
|
45 |
+
console.error('Error generating audio:', JSON.stringify(error.response.data));
|
46 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
47 |
+
status: 500,
|
48 |
+
headers: {
|
49 |
+
'Content-Type': 'application/json',
|
50 |
+
...corsHeaders
|
51 |
+
}
|
52 |
+
});
|
53 |
+
}
|
54 |
+
}
|
55 |
+
|
56 |
+
export async function OPTIONS(request: Request) {
|
57 |
+
return new Response(null, {
|
58 |
+
status: 200,
|
59 |
+
headers: corsHeaders
|
60 |
+
});
|
61 |
+
}
|
src/lib/SunoApi.ts
ADDED
@@ -0,0 +1,407 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import axios, { AxiosInstance } from 'axios';
|
2 |
+
import UserAgent from 'user-agents';
|
3 |
+
import pino from 'pino';
|
4 |
+
import { wrapper } from "axios-cookiejar-support";
|
5 |
+
import { CookieJar } from "tough-cookie";
|
6 |
+
import { sleep } from "@/lib/utils";
|
7 |
+
|
8 |
+
const logger = pino();
|
9 |
+
export const DEFAULT_MODEL = "chirp-v3-5";
|
10 |
+
|
11 |
+
|
12 |
+
export interface AudioInfo {
|
13 |
+
id: string; // Unique identifier for the audio
|
14 |
+
title?: string; // Title of the audio
|
15 |
+
image_url?: string; // URL of the image associated with the audio
|
16 |
+
lyric?: string; // Lyrics of the audio
|
17 |
+
audio_url?: string; // URL of the audio file
|
18 |
+
video_url?: string; // URL of the video associated with the audio
|
19 |
+
created_at: string; // Date and time when the audio was created
|
20 |
+
model_name: string; // Name of the model used for audio generation
|
21 |
+
gpt_description_prompt?: string; // Prompt for GPT description
|
22 |
+
prompt?: string; // Prompt for audio generation
|
23 |
+
status: string; // Status
|
24 |
+
type?: string;
|
25 |
+
tags?: string; // Genre of music.
|
26 |
+
duration?: string; // Duration of the audio
|
27 |
+
error_message?: string; // Error message if any
|
28 |
+
}
|
29 |
+
|
30 |
+
class SunoApi {
|
31 |
+
private static BASE_URL: string = 'https://studio-api.suno.ai';
|
32 |
+
private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
|
33 |
+
|
34 |
+
private readonly client: AxiosInstance;
|
35 |
+
private sid?: string;
|
36 |
+
private currentToken?: string;
|
37 |
+
|
38 |
+
constructor(cookie: string) {
|
39 |
+
const cookieJar = new CookieJar();
|
40 |
+
const randomUserAgent = new UserAgent(/Chrome/).random().toString();
|
41 |
+
this.client = wrapper(axios.create({
|
42 |
+
jar: cookieJar,
|
43 |
+
withCredentials: true,
|
44 |
+
headers: {
|
45 |
+
'User-Agent': randomUserAgent,
|
46 |
+
'Cookie': cookie
|
47 |
+
}
|
48 |
+
}))
|
49 |
+
this.client.interceptors.request.use((config) => {
|
50 |
+
if (this.currentToken) { // Use the current token status
|
51 |
+
config.headers['Authorization'] = `Bearer ${this.currentToken}`;
|
52 |
+
}
|
53 |
+
return config;
|
54 |
+
});
|
55 |
+
}
|
56 |
+
|
57 |
+
public async init(): Promise<SunoApi> {
|
58 |
+
await this.getAuthToken();
|
59 |
+
await this.keepAlive();
|
60 |
+
return this;
|
61 |
+
}
|
62 |
+
|
63 |
+
/**
|
64 |
+
* Get the session ID and save it for later use.
|
65 |
+
*/
|
66 |
+
private async getAuthToken() {
|
67 |
+
// URL to get session ID
|
68 |
+
const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_clerk_js_version=4.73.4`;
|
69 |
+
// Get session ID
|
70 |
+
const sessionResponse = await this.client.get(getSessionUrl);
|
71 |
+
if (!sessionResponse?.data?.response?.['last_active_session_id']) {
|
72 |
+
throw new Error("Failed to get session id, you may need to update the SUNO_COOKIE");
|
73 |
+
}
|
74 |
+
// Save session ID for later use
|
75 |
+
this.sid = sessionResponse.data.response['last_active_session_id'];
|
76 |
+
}
|
77 |
+
|
78 |
+
/**
|
79 |
+
* Keep the session alive.
|
80 |
+
* @param isWait Indicates if the method should wait for the session to be fully renewed before returning.
|
81 |
+
*/
|
82 |
+
public async keepAlive(isWait?: boolean): Promise<void> {
|
83 |
+
if (!this.sid) {
|
84 |
+
throw new Error("Session ID is not set. Cannot renew token.");
|
85 |
+
}
|
86 |
+
// URL to renew session token
|
87 |
+
const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens?_clerk_js_version==4.73.4`;
|
88 |
+
// Renew session token
|
89 |
+
const renewResponse = await this.client.post(renewUrl);
|
90 |
+
logger.info("KeepAlive...\n");
|
91 |
+
if (isWait) {
|
92 |
+
await sleep(1, 2);
|
93 |
+
}
|
94 |
+
const newToken = renewResponse.data['jwt'];
|
95 |
+
// Update Authorization field in request header with the new JWT token
|
96 |
+
this.currentToken = newToken;
|
97 |
+
}
|
98 |
+
|
99 |
+
/**
|
100 |
+
* Generate a song based on the prompt.
|
101 |
+
* @param prompt The text prompt to generate audio from.
|
102 |
+
* @param make_instrumental Indicates if the generated audio should be instrumental.
|
103 |
+
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
|
104 |
+
* @returns
|
105 |
+
*/
|
106 |
+
public async generate(
|
107 |
+
prompt: string,
|
108 |
+
make_instrumental: boolean = false,
|
109 |
+
model?: string,
|
110 |
+
wait_audio: boolean = false,
|
111 |
+
|
112 |
+
): Promise<AudioInfo[]> {
|
113 |
+
await this.keepAlive(false);
|
114 |
+
const startTime = Date.now();
|
115 |
+
const audios = this.generateSongs(prompt, false, undefined, undefined, make_instrumental, model, wait_audio);
|
116 |
+
const costTime = Date.now() - startTime;
|
117 |
+
logger.info("Generate Response:\n" + JSON.stringify(audios, null, 2));
|
118 |
+
logger.info("Cost time: " + costTime);
|
119 |
+
return audios;
|
120 |
+
}
|
121 |
+
|
122 |
+
/**
|
123 |
+
* Calls the concatenate endpoint for a clip to generate the whole song.
|
124 |
+
* @param clip_id The ID of the audio clip to concatenate.
|
125 |
+
* @returns A promise that resolves to an AudioInfo object representing the concatenated audio.
|
126 |
+
* @throws Error if the response status is not 200.
|
127 |
+
*/
|
128 |
+
public async concatenate(clip_id: string): Promise<AudioInfo> {
|
129 |
+
await this.keepAlive(false);
|
130 |
+
const payload: any = { clip_id: clip_id };
|
131 |
+
|
132 |
+
const response = await this.client.post(
|
133 |
+
`${SunoApi.BASE_URL}/api/generate/concat/v2/`,
|
134 |
+
payload,
|
135 |
+
{
|
136 |
+
timeout: 10000, // 10 seconds timeout
|
137 |
+
},
|
138 |
+
);
|
139 |
+
if (response.status !== 200) {
|
140 |
+
throw new Error("Error response:" + response.statusText);
|
141 |
+
}
|
142 |
+
return response.data;
|
143 |
+
}
|
144 |
+
|
145 |
+
/**
|
146 |
+
* Generates custom audio based on provided parameters.
|
147 |
+
*
|
148 |
+
* @param prompt The text prompt to generate audio from.
|
149 |
+
* @param tags Tags to categorize the generated audio.
|
150 |
+
* @param title The title for the generated audio.
|
151 |
+
* @param make_instrumental Indicates if the generated audio should be instrumental.
|
152 |
+
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
|
153 |
+
* @returns A promise that resolves to an array of AudioInfo objects representing the generated audios.
|
154 |
+
*/
|
155 |
+
public async custom_generate(
|
156 |
+
prompt: string,
|
157 |
+
tags: string,
|
158 |
+
title: string,
|
159 |
+
make_instrumental: boolean = false,
|
160 |
+
model?: string,
|
161 |
+
wait_audio: boolean = false,
|
162 |
+
): Promise<AudioInfo[]> {
|
163 |
+
const startTime = Date.now();
|
164 |
+
const audios = await this.generateSongs(prompt, true, tags, title, make_instrumental, model, wait_audio);
|
165 |
+
const costTime = Date.now() - startTime;
|
166 |
+
logger.info("Custom Generate Response:\n" + JSON.stringify(audios, null, 2));
|
167 |
+
logger.info("Cost time: " + costTime);
|
168 |
+
return audios;
|
169 |
+
}
|
170 |
+
|
171 |
+
/**
|
172 |
+
* Generates songs based on the provided parameters.
|
173 |
+
*
|
174 |
+
* @param prompt The text prompt to generate songs from.
|
175 |
+
* @param isCustom Indicates if the generation should consider custom parameters like tags and title.
|
176 |
+
* @param tags Optional tags to categorize the song, used only if isCustom is true.
|
177 |
+
* @param title Optional title for the song, used only if isCustom is true.
|
178 |
+
* @param make_instrumental Indicates if the generated song should be instrumental.
|
179 |
+
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
|
180 |
+
* @returns A promise that resolves to an array of AudioInfo objects representing the generated songs.
|
181 |
+
*/
|
182 |
+
private async generateSongs(
|
183 |
+
prompt: string,
|
184 |
+
isCustom: boolean,
|
185 |
+
tags?: string,
|
186 |
+
title?: string,
|
187 |
+
make_instrumental?: boolean,
|
188 |
+
model?: string,
|
189 |
+
wait_audio: boolean = false
|
190 |
+
): Promise<AudioInfo[]> {
|
191 |
+
await this.keepAlive(false);
|
192 |
+
const payload: any = {
|
193 |
+
make_instrumental: make_instrumental == true,
|
194 |
+
mv: model || DEFAULT_MODEL,
|
195 |
+
prompt: "",
|
196 |
+
};
|
197 |
+
if (isCustom) {
|
198 |
+
payload.tags = tags;
|
199 |
+
payload.title = title;
|
200 |
+
payload.prompt = prompt;
|
201 |
+
} else {
|
202 |
+
payload.gpt_description_prompt = prompt;
|
203 |
+
}
|
204 |
+
logger.info("generateSongs payload:\n" + JSON.stringify({
|
205 |
+
prompt: prompt,
|
206 |
+
isCustom: isCustom,
|
207 |
+
tags: tags,
|
208 |
+
title: title,
|
209 |
+
make_instrumental: make_instrumental,
|
210 |
+
wait_audio: wait_audio,
|
211 |
+
payload: payload,
|
212 |
+
}, null, 2));
|
213 |
+
const response = await this.client.post(
|
214 |
+
`${SunoApi.BASE_URL}/api/generate/v2/`,
|
215 |
+
payload,
|
216 |
+
{
|
217 |
+
timeout: 10000, // 10 seconds timeout
|
218 |
+
},
|
219 |
+
);
|
220 |
+
logger.info("generateSongs Response:\n" + JSON.stringify(response.data, null, 2));
|
221 |
+
if (response.status !== 200) {
|
222 |
+
throw new Error("Error response:" + response.statusText);
|
223 |
+
}
|
224 |
+
const songIds = response.data['clips'].map((audio: any) => audio.id);
|
225 |
+
//Want to wait for music file generation
|
226 |
+
if (wait_audio) {
|
227 |
+
const startTime = Date.now();
|
228 |
+
let lastResponse: AudioInfo[] = [];
|
229 |
+
await sleep(5, 5);
|
230 |
+
while (Date.now() - startTime < 100000) {
|
231 |
+
const response = await this.get(songIds);
|
232 |
+
const allCompleted = response.every(
|
233 |
+
audio => audio.status === 'streaming' || audio.status === 'complete'
|
234 |
+
);
|
235 |
+
const allError = response.every(
|
236 |
+
audio => audio.status === 'error'
|
237 |
+
);
|
238 |
+
if (allCompleted || allError) {
|
239 |
+
return response;
|
240 |
+
}
|
241 |
+
lastResponse = response;
|
242 |
+
await sleep(3, 6);
|
243 |
+
await this.keepAlive(true);
|
244 |
+
}
|
245 |
+
return lastResponse;
|
246 |
+
} else {
|
247 |
+
await this.keepAlive(true);
|
248 |
+
return response.data['clips'].map((audio: any) => ({
|
249 |
+
id: audio.id,
|
250 |
+
title: audio.title,
|
251 |
+
image_url: audio.image_url,
|
252 |
+
lyric: audio.metadata.prompt,
|
253 |
+
audio_url: audio.audio_url,
|
254 |
+
video_url: audio.video_url,
|
255 |
+
created_at: audio.created_at,
|
256 |
+
model_name: audio.model_name,
|
257 |
+
status: audio.status,
|
258 |
+
gpt_description_prompt: audio.metadata.gpt_description_prompt,
|
259 |
+
prompt: audio.metadata.prompt,
|
260 |
+
type: audio.metadata.type,
|
261 |
+
tags: audio.metadata.tags,
|
262 |
+
duration: audio.metadata.duration,
|
263 |
+
}));
|
264 |
+
}
|
265 |
+
}
|
266 |
+
|
267 |
+
/**
|
268 |
+
* Generates lyrics based on a given prompt.
|
269 |
+
* @param prompt The prompt for generating lyrics.
|
270 |
+
* @returns The generated lyrics text.
|
271 |
+
*/
|
272 |
+
public async generateLyrics(prompt: string): Promise<string> {
|
273 |
+
await this.keepAlive(false);
|
274 |
+
// Initiate lyrics generation
|
275 |
+
const generateResponse = await this.client.post(`${SunoApi.BASE_URL}/api/generate/lyrics/`, { prompt });
|
276 |
+
const generateId = generateResponse.data.id;
|
277 |
+
|
278 |
+
// Poll for lyrics completion
|
279 |
+
let lyricsResponse = await this.client.get(`${SunoApi.BASE_URL}/api/generate/lyrics/${generateId}`);
|
280 |
+
while (lyricsResponse?.data?.status !== 'complete') {
|
281 |
+
await sleep(2); // Wait for 2 seconds before polling again
|
282 |
+
lyricsResponse = await this.client.get(`${SunoApi.BASE_URL}/api/generate/lyrics/${generateId}`);
|
283 |
+
}
|
284 |
+
|
285 |
+
// Return the generated lyrics text
|
286 |
+
return lyricsResponse.data;
|
287 |
+
}
|
288 |
+
|
289 |
+
/**
|
290 |
+
* Extends an existing audio clip by generating additional content based on the provided prompt.
|
291 |
+
*
|
292 |
+
* @param audioId The ID of the audio clip to extend.
|
293 |
+
* @param prompt The prompt for generating additional content.
|
294 |
+
* @param continueAt Extend a new clip from a song at mm:ss(e.g. 00:30). Default extends from the end of the song.
|
295 |
+
* @param tags Style of Music.
|
296 |
+
* @param title Title of the song.
|
297 |
+
* @returns A promise that resolves to an AudioInfo object representing the extended audio clip.
|
298 |
+
*/
|
299 |
+
public async extendAudio(
|
300 |
+
audioId: string,
|
301 |
+
prompt: string = "",
|
302 |
+
continueAt: string = "0",
|
303 |
+
tags: string = "",
|
304 |
+
title: string = "",
|
305 |
+
model?: string,
|
306 |
+
): Promise<AudioInfo> {
|
307 |
+
const response = await this.client.post(`${SunoApi.BASE_URL}/api/generate/v2/`, {
|
308 |
+
continue_clip_id: audioId,
|
309 |
+
continue_at: continueAt,
|
310 |
+
mv: model || DEFAULT_MODEL,
|
311 |
+
prompt: prompt,
|
312 |
+
tags: tags,
|
313 |
+
title: title
|
314 |
+
});
|
315 |
+
console.log("response:\n", response);
|
316 |
+
return response.data;
|
317 |
+
}
|
318 |
+
|
319 |
+
/**
|
320 |
+
* Processes the lyrics (prompt) from the audio metadata into a more readable format.
|
321 |
+
* @param prompt The original lyrics text.
|
322 |
+
* @returns The processed lyrics text.
|
323 |
+
*/
|
324 |
+
private parseLyrics(prompt: string): string {
|
325 |
+
// Assuming the original lyrics are separated by a specific delimiter (e.g., newline), we can convert it into a more readable format.
|
326 |
+
// The implementation here can be adjusted according to the actual lyrics format.
|
327 |
+
// For example, if the lyrics exist as continuous text, it might be necessary to split them based on specific markers (such as periods, commas, etc.).
|
328 |
+
// The following implementation assumes that the lyrics are already separated by newlines.
|
329 |
+
|
330 |
+
// Split the lyrics using newline and ensure to remove empty lines.
|
331 |
+
const lines = prompt.split('\n').filter(line => line.trim() !== '');
|
332 |
+
|
333 |
+
// Reassemble the processed lyrics lines into a single string, separated by newlines between each line.
|
334 |
+
// Additional formatting logic can be added here, such as adding specific markers or handling special lines.
|
335 |
+
return lines.join('\n');
|
336 |
+
}
|
337 |
+
|
338 |
+
/**
|
339 |
+
* Retrieves audio information for the given song IDs.
|
340 |
+
* @param songIds An optional array of song IDs to retrieve information for.
|
341 |
+
* @returns A promise that resolves to an array of AudioInfo objects.
|
342 |
+
*/
|
343 |
+
public async get(songIds?: string[]): Promise<AudioInfo[]> {
|
344 |
+
await this.keepAlive(false);
|
345 |
+
let url = `${SunoApi.BASE_URL}/api/feed/`;
|
346 |
+
if (songIds) {
|
347 |
+
url = `${url}?ids=${songIds.join(',')}`;
|
348 |
+
}
|
349 |
+
logger.info("Get audio status: " + url);
|
350 |
+
const response = await this.client.get(url, {
|
351 |
+
// 3 seconds timeout
|
352 |
+
timeout: 3000
|
353 |
+
});
|
354 |
+
|
355 |
+
const audios = response.data;
|
356 |
+
return audios.map((audio: any) => ({
|
357 |
+
id: audio.id,
|
358 |
+
title: audio.title,
|
359 |
+
image_url: audio.image_url,
|
360 |
+
lyric: audio.metadata.prompt ? this.parseLyrics(audio.metadata.prompt) : "",
|
361 |
+
audio_url: audio.audio_url,
|
362 |
+
video_url: audio.video_url,
|
363 |
+
created_at: audio.created_at,
|
364 |
+
model_name: audio.model_name,
|
365 |
+
status: audio.status,
|
366 |
+
gpt_description_prompt: audio.metadata.gpt_description_prompt,
|
367 |
+
prompt: audio.metadata.prompt,
|
368 |
+
type: audio.metadata.type,
|
369 |
+
tags: audio.metadata.tags,
|
370 |
+
duration: audio.metadata.duration,
|
371 |
+
error_message: audio.metadata.error_message,
|
372 |
+
}));
|
373 |
+
}
|
374 |
+
|
375 |
+
/**
|
376 |
+
* Retrieves information for a specific audio clip.
|
377 |
+
* @param clipId The ID of the audio clip to retrieve information for.
|
378 |
+
* @returns A promise that resolves to an object containing the audio clip information.
|
379 |
+
*/
|
380 |
+
public async getClip(clipId: string): Promise<object> {
|
381 |
+
await this.keepAlive(false);
|
382 |
+
const response = await this.client.get(`${SunoApi.BASE_URL}/api/clip/${clipId}`);
|
383 |
+
return response.data;
|
384 |
+
}
|
385 |
+
|
386 |
+
public async get_credits(): Promise<object> {
|
387 |
+
await this.keepAlive(false);
|
388 |
+
const response = await this.client.get(`${SunoApi.BASE_URL}/api/billing/info/`);
|
389 |
+
return {
|
390 |
+
credits_left: response.data.total_credits_left,
|
391 |
+
period: response.data.period,
|
392 |
+
monthly_limit: response.data.monthly_limit,
|
393 |
+
monthly_usage: response.data.monthly_usage,
|
394 |
+
};
|
395 |
+
}
|
396 |
+
}
|
397 |
+
|
398 |
+
const newSunoApi = async (cookie: string) => {
|
399 |
+
const sunoApi = new SunoApi(cookie);
|
400 |
+
return await sunoApi.init();
|
401 |
+
}
|
402 |
+
|
403 |
+
if (!process.env.SUNO_COOKIE) {
|
404 |
+
console.log("Environment does not contain SUNO_COOKIE.", process.env)
|
405 |
+
}
|
406 |
+
|
407 |
+
export const sunoApi = newSunoApi(process.env.SUNO_COOKIE || '');
|
src/lib/utils.ts
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pino from "pino";
|
2 |
+
|
3 |
+
const logger = pino();
|
4 |
+
|
5 |
+
/**
|
6 |
+
* Pause for a specified number of seconds.
|
7 |
+
* @param x Minimum number of seconds.
|
8 |
+
* @param y Maximum number of seconds (optional).
|
9 |
+
*/
|
10 |
+
export const sleep = (x: number, y?: number): Promise<void> => {
|
11 |
+
let timeout = x * 1000;
|
12 |
+
if (y !== undefined && y !== x) {
|
13 |
+
const min = Math.min(x, y);
|
14 |
+
const max = Math.max(x, y);
|
15 |
+
timeout = Math.floor(Math.random() * (max - min + 1) + min) * 1000;
|
16 |
+
}
|
17 |
+
// console.log(`Sleeping for ${timeout / 1000} seconds`);
|
18 |
+
logger.info(`Sleeping for ${timeout / 1000} seconds`);
|
19 |
+
|
20 |
+
return new Promise(resolve => setTimeout(resolve, timeout));
|
21 |
+
}
|
22 |
+
|
23 |
+
|
24 |
+
export const corsHeaders = {
|
25 |
+
'Access-Control-Allow-Origin': '*',
|
26 |
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
27 |
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
28 |
+
}
|
tailwind.config.ts
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Config } from "tailwindcss";
|
2 |
+
|
3 |
+
const config: Config = {
|
4 |
+
content: [
|
5 |
+
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
6 |
+
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
7 |
+
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
8 |
+
],
|
9 |
+
theme: {
|
10 |
+
extend: {
|
11 |
+
backgroundImage: {
|
12 |
+
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
13 |
+
"gradient-conic":
|
14 |
+
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
15 |
+
},
|
16 |
+
},
|
17 |
+
},
|
18 |
+
plugins: [
|
19 |
+
require('@tailwindcss/typography'),
|
20 |
+
],
|
21 |
+
};
|
22 |
+
export default config;
|