rippanteq7 commited on
Commit
dfe72e0
·
verified ·
1 Parent(s): 49c8682

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +43 -0
  2. .env.example +2 -0
  3. .eslintrc.json +3 -0
  4. .gitattributes +2 -0
  5. .github/ISSUE_TEMPLATE/bug_report.md +32 -0
  6. .github/ISSUE_TEMPLATE/feature_request.md +20 -0
  7. .github/ISSUE_TEMPLATE/新功能.md +21 -0
  8. .gitignore +39 -0
  9. Dockerfile +21 -0
  10. FUNDING.yml +14 -0
  11. LICENSE +165 -0
  12. README.md +327 -12
  13. README_CN.md +304 -0
  14. docker-compose.yml +12 -0
  15. icon.png +0 -0
  16. next.config.mjs +4 -0
  17. package.json +47 -0
  18. pnpm-lock.yaml +0 -0
  19. postcss.config.js +6 -0
  20. public/get-cookie-demo.gif +3 -0
  21. public/get-cookie-demo.mp4 +3 -0
  22. public/github-logo.webp +0 -0
  23. public/github-mark.png +0 -0
  24. public/next.svg +1 -0
  25. public/suno-banner.png +0 -0
  26. public/swagger-suno-api.json +257 -0
  27. public/vercel.svg +1 -0
  28. src/app/api/clip/route.ts +58 -0
  29. src/app/api/concat/route.ts +64 -0
  30. src/app/api/custom_generate/route.ts +61 -0
  31. src/app/api/extend_audio/route.ts +69 -0
  32. src/app/api/generate/route.ts +63 -0
  33. src/app/api/generate_lyrics/route.ts +57 -0
  34. src/app/api/get/route.ts +54 -0
  35. src/app/api/get_limit/route.ts +48 -0
  36. src/app/components/Footer.tsx +24 -0
  37. src/app/components/Header.tsx +50 -0
  38. src/app/components/Logo.tsx +13 -0
  39. src/app/components/Section.tsx +22 -0
  40. src/app/components/Swagger.tsx +15 -0
  41. src/app/docs/page.tsx +59 -0
  42. src/app/docs/swagger-suno-api.json +600 -0
  43. src/app/favicon.ico +0 -0
  44. src/app/globals.css +25 -0
  45. src/app/layout.tsx +34 -0
  46. src/app/page.tsx +154 -0
  47. src/app/v1/chat/completions/route.ts +61 -0
  48. src/lib/SunoApi.ts +407 -0
  49. src/lib/utils.ts +28 -0
  50. 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
- title: Suno
3
- emoji: 📉
4
- colorFrom: purple
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 4.41.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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&#0045;art&#0045;suno&#0045;api&#0045;open&#0045;source&#0045;sunoai&#0045;api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/product_review.svg?product_id=577408&theme=light" alt="gcui&#0045;art&#0047;suno&#0045;api&#0058;Open&#0045;source&#0032;SunoAI&#0032;API - Use&#0032;API&#0032;to&#0032;call&#0032;the&#0032;music&#0032;generation&#0032;AI&#0032;of&#0032;suno&#0046;ai&#0046; | 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
+ ![suno-api banner](https://github.com/gcui-art/suno-api/blob/main/public/suno-banner.png)
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
+ ![get cookie](https://github.com/gcui-art/suno-api/blob/main/public/get-cookie-demo.gif)
57
+
58
+ ### 2. Clone and deploy this project
59
+
60
+ You can choose your preferred deployment method:
61
+
62
+ #### Deploy to Vercel
63
+
64
+ [![Deploy with Vercel](https://vercel.com/button)](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&#0045;art&#0045;suno&#0045;api&#0045;open&#0045;source&#0045;sunoai&#0045;api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/product_review.svg?product_id=577408&theme=light" alt="gcui&#0045;art&#0047;suno&#0045;api&#0058;Open&#0045;source&#0032;SunoAI&#0032;API - Use&#0032;API&#0032;to&#0032;call&#0032;the&#0032;music&#0032;generation&#0032;AI&#0032;of&#0032;suno&#0046;ai&#0046; | 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
+ ![suno-api banner](https://github.com/gcui-art/suno-api/blob/main/public/suno-banner.png)
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
+ ![获取cookie](https://github.com/gcui-art/suno-api/blob/main/public/get-cookie-demo.gif)
56
+
57
+ ### 2. 克隆并部署本项目
58
+
59
+ 你可以选择自己喜欢的部署方式:
60
+
61
+ #### 部署到 Vercel
62
+
63
+ [![部署到 Vercel](https://vercel.com/button)](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

  • SHA256: 388372f371032eda2433bbd204e0415477426f942f13479c7115c0b01d414046
  • Pointer size: 133 Bytes
  • Size of remote file: 44.6 MB
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
+ [![Deploy with Vercel](https://vercel.com/button)](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![Song Cover](${audio.image_url})\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;