Upload 49 files
Browse files- .dockerignore +43 -0
- .env.example +7 -0
- .eslintrc.json +3 -0
- .gitattributes +2 -0
- .gitignore +39 -0
- .prettierrc +4 -0
- Dockerfile +32 -0
- README.md +11 -11
- docker-compose.yml +13 -0
- icon.png +0 -0
- next.config.mjs +15 -0
- package-lock.json +0 -0
- package.json +58 -0
- pnpm-lock.yaml +0 -0
- postcss.config.js +6 -0
- public/drag-instructions.jpg +0 -0
- public/get-cookie-demo.gif +3 -0
- public/get-cookie-demo.mp4 +3 -0
- public/github-logo.webp +0 -0
- public/github-mark.png +0 -0
- public/next.svg +1 -0
- public/swagger-suno-api.json +257 -0
- public/vercel.svg +1 -0
- src/app/api/clip/route.ts +58 -0
- src/app/api/concat/route.ts +65 -0
- src/app/api/custom_generate/route.ts +54 -0
- src/app/api/extend_audio/route.ts +70 -0
- src/app/api/generate/route.ts +64 -0
- src/app/api/generate_lyrics/route.ts +58 -0
- src/app/api/generate_stems/route.ts +70 -0
- src/app/api/get/route.ts +61 -0
- src/app/api/get_aligned_lyrics/route.ts +61 -0
- src/app/api/get_limit/route.ts +49 -0
- src/app/api/persona/route.ts +61 -0
- src/app/components/Footer.tsx +24 -0
- src/app/components/Header.tsx +50 -0
- src/app/components/Logo.tsx +13 -0
- src/app/components/Section.tsx +22 -0
- src/app/components/Swagger.tsx +20 -0
- src/app/docs/page.tsx +62 -0
- src/app/docs/swagger-suno-api.json +814 -0
- src/app/favicon.ico +0 -0
- src/app/globals.css +25 -0
- src/app/layout.tsx +34 -0
- src/app/page.tsx +156 -0
- src/app/v1/chat/completions/route.ts +61 -0
- src/lib/SunoApi.ts +870 -0
- src/lib/utils.ts +118 -0
- tailwind.config.ts +22 -0
- tsconfig.json +29 -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,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# For more information, please see the README.md
|
| 2 |
+
SUNO_COOKIE=
|
| 3 |
+
TWOCAPTCHA_KEY= # Obtain from 2captcha.com
|
| 4 |
+
BROWSER=chromium # `chromium` or `firefox`, although `chromium` is highly recommended
|
| 5 |
+
BROWSER_GHOST_CURSOR=false
|
| 6 |
+
BROWSER_LOCALE=en
|
| 7 |
+
BROWSER_HEADLESS=true
|
.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
|
.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
|
.prettierrc
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"trailingComma": "none",
|
| 3 |
+
"singleQuote": true
|
| 4 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1
|
| 2 |
+
|
| 3 |
+
FROM node:lts-bookworm 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-bookworm
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
COPY package*.json ./
|
| 13 |
+
|
| 14 |
+
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y libnss3 \
|
| 15 |
+
libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
|
| 16 |
+
libgbm1 libxkbcommon0 libasound2 libcups2 xvfb
|
| 17 |
+
|
| 18 |
+
ARG SUNO_COOKIE
|
| 19 |
+
RUN if [ -z "$SUNO_COOKIE" ]; then echo "Warning: SUNO_COOKIE is not set. You will have to set the cookies in the Cookie header of your requests."; fi
|
| 20 |
+
ENV SUNO_COOKIE=${SUNO_COOKIE}
|
| 21 |
+
# Disable GPU acceleration, as with it suno-api won't work in a Docker environment
|
| 22 |
+
ENV BROWSER_DISABLE_GPU=true
|
| 23 |
+
|
| 24 |
+
RUN npm install --only=production
|
| 25 |
+
|
| 26 |
+
# Install all supported browsers, else switching browsers requires an image rebuild
|
| 27 |
+
RUN npx playwright install chromium
|
| 28 |
+
# RUN npx playwright install firefox
|
| 29 |
+
|
| 30 |
+
COPY --from=builder /src/.next ./.next
|
| 31 |
+
EXPOSE 3000
|
| 32 |
+
CMD ["npm", "run", "start"]
|
README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: N M Api
|
| 3 |
-
emoji: 🏃
|
| 4 |
-
colorFrom: gray
|
| 5 |
-
colorTo: yellow
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
license: mit
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: N M Api
|
| 3 |
+
emoji: 🏃
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: yellow
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 13 |
+
env_file: ".env"
|
icon.png
ADDED
|
|
next.config.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('next').NextConfig} */
|
| 2 |
+
const nextConfig = {
|
| 3 |
+
webpack: (config) => {
|
| 4 |
+
config.module.rules.push({
|
| 5 |
+
test: /\.(ttf|html)$/i,
|
| 6 |
+
type: 'asset/resource'
|
| 7 |
+
});
|
| 8 |
+
return config;
|
| 9 |
+
},
|
| 10 |
+
experimental: {
|
| 11 |
+
serverMinification: false, // the server minification unfortunately breaks the selector class names
|
| 12 |
+
},
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
export default nextConfig;
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"@2captcha/captcha-solver": "^1.3.0",
|
| 19 |
+
"@playwright/browser-chromium": "^1.49.1",
|
| 20 |
+
"@vercel/analytics": "^1.2.2",
|
| 21 |
+
"axios": "^1.7.8",
|
| 22 |
+
"bufferutil": "^4.0.8",
|
| 23 |
+
"chromium-bidi": "^0.10.1",
|
| 24 |
+
"cookie": "^1.0.2",
|
| 25 |
+
"electron": "^33.2.1",
|
| 26 |
+
"ghost-cursor-playwright": "^2.1.0",
|
| 27 |
+
"js-cookie": "^3.0.5",
|
| 28 |
+
"next": "14.1.4",
|
| 29 |
+
"next-swagger-doc": "^0.4.0",
|
| 30 |
+
"pino": "^8.19.0",
|
| 31 |
+
"pino-pretty": "^11.0.0",
|
| 32 |
+
"react": "^18",
|
| 33 |
+
"react-dom": "^18",
|
| 34 |
+
"react-markdown": "^9.0.1",
|
| 35 |
+
"rebrowser-playwright-core": "^1.49.1",
|
| 36 |
+
"swagger-ui-react": "^5.18.2",
|
| 37 |
+
"tough-cookie": "^4.1.4",
|
| 38 |
+
"user-agents": "^1.1.156",
|
| 39 |
+
"utf-8-validate": "^6.0.5",
|
| 40 |
+
"yn": "^5.0.0"
|
| 41 |
+
},
|
| 42 |
+
"devDependencies": {
|
| 43 |
+
"@tailwindcss/typography": "^0.5.12",
|
| 44 |
+
"@types/js-cookie": "^3.0.6",
|
| 45 |
+
"@types/node": "^20",
|
| 46 |
+
"@types/react": "^18",
|
| 47 |
+
"@types/react-dom": "^18",
|
| 48 |
+
"@types/swagger-ui-react": "^4.18.3",
|
| 49 |
+
"@types/tough-cookie": "^4.0.5",
|
| 50 |
+
"@types/user-agents": "^1.0.4",
|
| 51 |
+
"autoprefixer": "^10.0.1",
|
| 52 |
+
"eslint": "^8.57.0",
|
| 53 |
+
"eslint-config-next": "14.1.4",
|
| 54 |
+
"postcss": "^8",
|
| 55 |
+
"tailwindcss": "^3.3.0",
|
| 56 |
+
"typescript": "^5"
|
| 57 |
+
}
|
| 58 |
+
}
|
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/drag-instructions.jpg
ADDED
|
public/get-cookie-demo.gif
ADDED
|
Git LFS Details
|
public/get-cookie-demo.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6ee1df7a41fdc5064253da0d13c69abb4af879a05b593f30157894e44ed84472
|
| 3 |
+
size 6762147
|
public/github-logo.webp
ADDED
|
public/github-mark.png
ADDED
|
public/next.svg
ADDED
|
|
public/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,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse, NextRequest } from "next/server";
|
| 2 |
+
import { cookies } from 'next/headers'
|
| 3 |
+
import { sunoApi } from "@/lib/SunoApi";
|
| 4 |
+
import { corsHeaders } from "@/lib/utils";
|
| 5 |
+
|
| 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 { clip_id } = body;
|
| 13 |
+
if (!clip_id) {
|
| 14 |
+
return new NextResponse(JSON.stringify({ error: 'Clip id is required' }), {
|
| 15 |
+
status: 400,
|
| 16 |
+
headers: {
|
| 17 |
+
'Content-Type': 'application/json',
|
| 18 |
+
...corsHeaders
|
| 19 |
+
}
|
| 20 |
+
});
|
| 21 |
+
}
|
| 22 |
+
const audioInfo = await (await sunoApi((await cookies()).toString())).concatenate(clip_id);
|
| 23 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
| 24 |
+
status: 200,
|
| 25 |
+
headers: {
|
| 26 |
+
'Content-Type': 'application/json',
|
| 27 |
+
...corsHeaders
|
| 28 |
+
}
|
| 29 |
+
});
|
| 30 |
+
} catch (error: any) {
|
| 31 |
+
console.error('Error generating concatenating audio:', error.response.data);
|
| 32 |
+
if (error.response.status === 402) {
|
| 33 |
+
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
| 34 |
+
status: 402,
|
| 35 |
+
headers: {
|
| 36 |
+
'Content-Type': 'application/json',
|
| 37 |
+
...corsHeaders
|
| 38 |
+
}
|
| 39 |
+
});
|
| 40 |
+
}
|
| 41 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
|
| 42 |
+
status: 500,
|
| 43 |
+
headers: {
|
| 44 |
+
'Content-Type': 'application/json',
|
| 45 |
+
...corsHeaders
|
| 46 |
+
}
|
| 47 |
+
});
|
| 48 |
+
}
|
| 49 |
+
} else {
|
| 50 |
+
return new NextResponse('Method Not Allowed', {
|
| 51 |
+
headers: {
|
| 52 |
+
Allow: 'POST',
|
| 53 |
+
...corsHeaders
|
| 54 |
+
},
|
| 55 |
+
status: 405
|
| 56 |
+
});
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export async function OPTIONS(request: Request) {
|
| 61 |
+
return new Response(null, {
|
| 62 |
+
status: 200,
|
| 63 |
+
headers: corsHeaders
|
| 64 |
+
});
|
| 65 |
+
}
|
src/app/api/custom_generate/route.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse, NextRequest } from "next/server";
|
| 2 |
+
import { cookies } from 'next/headers';
|
| 3 |
+
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
| 4 |
+
import { corsHeaders } from "@/lib/utils";
|
| 5 |
+
|
| 6 |
+
export const maxDuration = 60; // allow longer timeout for wait_audio == true
|
| 7 |
+
export const dynamic = "force-dynamic";
|
| 8 |
+
|
| 9 |
+
export async function POST(req: NextRequest) {
|
| 10 |
+
if (req.method === 'POST') {
|
| 11 |
+
try {
|
| 12 |
+
const body = await req.json();
|
| 13 |
+
const { prompt, tags, title, make_instrumental, model, wait_audio, negative_tags } = body;
|
| 14 |
+
const audioInfo = await (await sunoApi((await cookies()).toString())).custom_generate(
|
| 15 |
+
prompt, tags, title,
|
| 16 |
+
Boolean(make_instrumental),
|
| 17 |
+
model || DEFAULT_MODEL,
|
| 18 |
+
Boolean(wait_audio),
|
| 19 |
+
negative_tags
|
| 20 |
+
);
|
| 21 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
| 22 |
+
status: 200,
|
| 23 |
+
headers: {
|
| 24 |
+
'Content-Type': 'application/json',
|
| 25 |
+
...corsHeaders
|
| 26 |
+
}
|
| 27 |
+
});
|
| 28 |
+
} catch (error: any) {
|
| 29 |
+
console.error('Error generating custom audio:', error);
|
| 30 |
+
return new NextResponse(JSON.stringify({ error: error.response?.data?.detail || error.toString() }), {
|
| 31 |
+
status: error.response?.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: 'POST',
|
| 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/extend_audio/route.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse, NextRequest } from "next/server";
|
| 2 |
+
import { cookies } from 'next/headers'
|
| 3 |
+
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
| 4 |
+
import { corsHeaders } from "@/lib/utils";
|
| 5 |
+
|
| 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 { audio_id, prompt, continue_at, tags, negative_tags, title, model, wait_audio } = body;
|
| 13 |
+
|
| 14 |
+
if (!audio_id) {
|
| 15 |
+
return new NextResponse(JSON.stringify({ error: 'Audio ID is required' }), {
|
| 16 |
+
status: 400,
|
| 17 |
+
headers: {
|
| 18 |
+
'Content-Type': 'application/json',
|
| 19 |
+
...corsHeaders
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const audioInfo = await (await sunoApi((await cookies()).toString()))
|
| 25 |
+
.extendAudio(audio_id, prompt, continue_at, tags || '', negative_tags || '', title, model || DEFAULT_MODEL, wait_audio || false);
|
| 26 |
+
|
| 27 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
| 28 |
+
status: 200,
|
| 29 |
+
headers: {
|
| 30 |
+
'Content-Type': 'application/json',
|
| 31 |
+
...corsHeaders
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
} catch (error: any) {
|
| 35 |
+
console.error('Error extend audio:', JSON.stringify(error.response.data));
|
| 36 |
+
if (error.response.status === 402) {
|
| 37 |
+
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
| 38 |
+
status: 402,
|
| 39 |
+
headers: {
|
| 40 |
+
'Content-Type': 'application/json',
|
| 41 |
+
...corsHeaders
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
| 46 |
+
status: 500,
|
| 47 |
+
headers: {
|
| 48 |
+
'Content-Type': 'application/json',
|
| 49 |
+
...corsHeaders
|
| 50 |
+
}
|
| 51 |
+
});
|
| 52 |
+
}
|
| 53 |
+
} else {
|
| 54 |
+
return new NextResponse('Method Not Allowed', {
|
| 55 |
+
headers: {
|
| 56 |
+
Allow: 'POST',
|
| 57 |
+
...corsHeaders
|
| 58 |
+
},
|
| 59 |
+
status: 405
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
export async function OPTIONS(request: Request) {
|
| 66 |
+
return new Response(null, {
|
| 67 |
+
status: 200,
|
| 68 |
+
headers: corsHeaders
|
| 69 |
+
});
|
| 70 |
+
}
|
src/app/api/generate/route.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse, NextRequest } from "next/server";
|
| 2 |
+
import { cookies } from 'next/headers'
|
| 3 |
+
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
| 4 |
+
import { corsHeaders } from "@/lib/utils";
|
| 5 |
+
|
| 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, make_instrumental, model, wait_audio } = body;
|
| 13 |
+
|
| 14 |
+
const audioInfo = await (await sunoApi((await cookies()).toString())).generate(
|
| 15 |
+
prompt,
|
| 16 |
+
Boolean(make_instrumental),
|
| 17 |
+
model || DEFAULT_MODEL,
|
| 18 |
+
Boolean(wait_audio)
|
| 19 |
+
);
|
| 20 |
+
|
| 21 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
| 22 |
+
status: 200,
|
| 23 |
+
headers: {
|
| 24 |
+
'Content-Type': 'application/json',
|
| 25 |
+
...corsHeaders
|
| 26 |
+
}
|
| 27 |
+
});
|
| 28 |
+
} catch (error: any) {
|
| 29 |
+
console.error('Error generating custom audio:', JSON.stringify(error.response.data));
|
| 30 |
+
if (error.response.status === 402) {
|
| 31 |
+
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
| 32 |
+
status: 402,
|
| 33 |
+
headers: {
|
| 34 |
+
'Content-Type': 'application/json',
|
| 35 |
+
...corsHeaders
|
| 36 |
+
}
|
| 37 |
+
});
|
| 38 |
+
}
|
| 39 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
| 40 |
+
status: 500,
|
| 41 |
+
headers: {
|
| 42 |
+
'Content-Type': 'application/json',
|
| 43 |
+
...corsHeaders
|
| 44 |
+
}
|
| 45 |
+
});
|
| 46 |
+
}
|
| 47 |
+
} else {
|
| 48 |
+
return new NextResponse('Method Not Allowed', {
|
| 49 |
+
headers: {
|
| 50 |
+
Allow: 'POST',
|
| 51 |
+
...corsHeaders
|
| 52 |
+
},
|
| 53 |
+
status: 405
|
| 54 |
+
});
|
| 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/generate_lyrics/route.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse, NextRequest } from "next/server";
|
| 2 |
+
import { cookies } from 'next/headers'
|
| 3 |
+
import { sunoApi } from "@/lib/SunoApi";
|
| 4 |
+
import { corsHeaders } from "@/lib/utils";
|
| 5 |
+
|
| 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 } = body;
|
| 13 |
+
|
| 14 |
+
const lyrics = await (await sunoApi((await cookies()).toString())).generateLyrics(prompt);
|
| 15 |
+
|
| 16 |
+
return new NextResponse(JSON.stringify(lyrics), {
|
| 17 |
+
status: 200,
|
| 18 |
+
headers: {
|
| 19 |
+
'Content-Type': 'application/json',
|
| 20 |
+
...corsHeaders
|
| 21 |
+
}
|
| 22 |
+
});
|
| 23 |
+
} catch (error: any) {
|
| 24 |
+
console.error('Error generating lyrics:', JSON.stringify(error.response.data));
|
| 25 |
+
if (error.response.status === 402) {
|
| 26 |
+
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
| 27 |
+
status: 402,
|
| 28 |
+
headers: {
|
| 29 |
+
'Content-Type': 'application/json',
|
| 30 |
+
...corsHeaders
|
| 31 |
+
}
|
| 32 |
+
});
|
| 33 |
+
}
|
| 34 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
| 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: 'POST',
|
| 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/generate_stems/route.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse, NextRequest } from "next/server";
|
| 2 |
+
import { cookies } from 'next/headers';
|
| 3 |
+
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
| 4 |
+
import { corsHeaders } from "@/lib/utils";
|
| 5 |
+
|
| 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 { audio_id } = body;
|
| 13 |
+
|
| 14 |
+
if (!audio_id) {
|
| 15 |
+
return new NextResponse(JSON.stringify({ error: 'Audio ID is required' }), {
|
| 16 |
+
status: 400,
|
| 17 |
+
headers: {
|
| 18 |
+
'Content-Type': 'application/json',
|
| 19 |
+
...corsHeaders
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const audioInfo = await (await sunoApi((await cookies()).toString()))
|
| 25 |
+
.generateStems(audio_id);
|
| 26 |
+
|
| 27 |
+
return new NextResponse(JSON.stringify(audioInfo), {
|
| 28 |
+
status: 200,
|
| 29 |
+
headers: {
|
| 30 |
+
'Content-Type': 'application/json',
|
| 31 |
+
...corsHeaders
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
} catch (error: any) {
|
| 35 |
+
console.error('Error generating stems:', JSON.stringify(error.response.data));
|
| 36 |
+
if (error.response.status === 402) {
|
| 37 |
+
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
|
| 38 |
+
status: 402,
|
| 39 |
+
headers: {
|
| 40 |
+
'Content-Type': 'application/json',
|
| 41 |
+
...corsHeaders
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
| 46 |
+
status: 500,
|
| 47 |
+
headers: {
|
| 48 |
+
'Content-Type': 'application/json',
|
| 49 |
+
...corsHeaders
|
| 50 |
+
}
|
| 51 |
+
});
|
| 52 |
+
}
|
| 53 |
+
} else {
|
| 54 |
+
return new NextResponse('Method Not Allowed', {
|
| 55 |
+
headers: {
|
| 56 |
+
Allow: 'POST',
|
| 57 |
+
...corsHeaders
|
| 58 |
+
},
|
| 59 |
+
status: 405
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
export async function OPTIONS(request: Request) {
|
| 66 |
+
return new Response(null, {
|
| 67 |
+
status: 200,
|
| 68 |
+
headers: corsHeaders
|
| 69 |
+
});
|
| 70 |
+
}
|
src/app/api/get/route.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse, NextRequest } from 'next/server';
|
| 2 |
+
import { cookies } from 'next/headers';
|
| 3 |
+
import { sunoApi } from '@/lib/SunoApi';
|
| 4 |
+
import { corsHeaders } from '@/lib/utils';
|
| 5 |
+
|
| 6 |
+
export const dynamic = 'force-dynamic';
|
| 7 |
+
|
| 8 |
+
export async function GET(req: NextRequest) {
|
| 9 |
+
if (req.method === 'GET') {
|
| 10 |
+
try {
|
| 11 |
+
const url = new URL(req.url);
|
| 12 |
+
const songIds = url.searchParams.get('ids');
|
| 13 |
+
const page = url.searchParams.get('page');
|
| 14 |
+
const cookie = (await cookies()).toString();
|
| 15 |
+
|
| 16 |
+
let audioInfo = [];
|
| 17 |
+
if (songIds && songIds.length > 0) {
|
| 18 |
+
const idsArray = songIds.split(',');
|
| 19 |
+
audioInfo = await (await sunoApi(cookie)).get(idsArray, page);
|
| 20 |
+
} else {
|
| 21 |
+
audioInfo = await (await sunoApi(cookie)).get(undefined, page);
|
| 22 |
+
}
|
| 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(
|
| 35 |
+
JSON.stringify({ error: 'Internal server error' }),
|
| 36 |
+
{
|
| 37 |
+
status: 500,
|
| 38 |
+
headers: {
|
| 39 |
+
'Content-Type': 'application/json',
|
| 40 |
+
...corsHeaders
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
);
|
| 44 |
+
}
|
| 45 |
+
} else {
|
| 46 |
+
return new NextResponse('Method Not Allowed', {
|
| 47 |
+
headers: {
|
| 48 |
+
Allow: 'GET',
|
| 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/get_aligned_lyrics/route.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse, NextRequest } from "next/server";
|
| 2 |
+
import { cookies } from 'next/headers'
|
| 3 |
+
import { sunoApi } from "@/lib/SunoApi";
|
| 4 |
+
import { corsHeaders } from "@/lib/utils";
|
| 5 |
+
|
| 6 |
+
export const dynamic = "force-dynamic";
|
| 7 |
+
|
| 8 |
+
export async function GET(req: NextRequest) {
|
| 9 |
+
if (req.method === 'GET') {
|
| 10 |
+
try {
|
| 11 |
+
const url = new URL(req.url);
|
| 12 |
+
const song_id = url.searchParams.get('song_id');
|
| 13 |
+
|
| 14 |
+
if (!song_id) {
|
| 15 |
+
return new NextResponse(JSON.stringify({ error: 'Song ID is required' }), {
|
| 16 |
+
status: 400,
|
| 17 |
+
headers: {
|
| 18 |
+
'Content-Type': 'application/json',
|
| 19 |
+
...corsHeaders
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const lyricAlignment = await (await sunoApi((await cookies()).toString())).getLyricAlignment(song_id);
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
return new NextResponse(JSON.stringify(lyricAlignment), {
|
| 28 |
+
status: 200,
|
| 29 |
+
headers: {
|
| 30 |
+
'Content-Type': 'application/json',
|
| 31 |
+
...corsHeaders
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
} catch (error) {
|
| 35 |
+
console.error('Error fetching lyric alignment:', error);
|
| 36 |
+
|
| 37 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error. ' + 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: 'GET',
|
| 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/get_limit/route.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse, NextRequest } from "next/server";
|
| 2 |
+
import { cookies } from 'next/headers'
|
| 3 |
+
import { sunoApi } from "@/lib/SunoApi";
|
| 4 |
+
import { corsHeaders } from "@/lib/utils";
|
| 5 |
+
|
| 6 |
+
export const dynamic = "force-dynamic";
|
| 7 |
+
|
| 8 |
+
export async function GET(req: NextRequest) {
|
| 9 |
+
if (req.method === 'GET') {
|
| 10 |
+
try {
|
| 11 |
+
|
| 12 |
+
const limit = await (await sunoApi((await cookies()).toString())).get_credits();
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
return new NextResponse(JSON.stringify(limit), {
|
| 16 |
+
status: 200,
|
| 17 |
+
headers: {
|
| 18 |
+
'Content-Type': 'application/json',
|
| 19 |
+
...corsHeaders
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
} catch (error) {
|
| 23 |
+
console.error('Error fetching limit:', error);
|
| 24 |
+
|
| 25 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error. ' + error }), {
|
| 26 |
+
status: 500,
|
| 27 |
+
headers: {
|
| 28 |
+
'Content-Type': 'application/json',
|
| 29 |
+
...corsHeaders
|
| 30 |
+
}
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
} else {
|
| 34 |
+
return new NextResponse('Method Not Allowed', {
|
| 35 |
+
headers: {
|
| 36 |
+
Allow: 'GET',
|
| 37 |
+
...corsHeaders
|
| 38 |
+
},
|
| 39 |
+
status: 405
|
| 40 |
+
});
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export async function OPTIONS(request: Request) {
|
| 45 |
+
return new Response(null, {
|
| 46 |
+
status: 200,
|
| 47 |
+
headers: corsHeaders
|
| 48 |
+
});
|
| 49 |
+
}
|
src/app/api/persona/route.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 personaId = url.searchParams.get('id');
|
| 12 |
+
const page = url.searchParams.get('page');
|
| 13 |
+
|
| 14 |
+
if (personaId == null) {
|
| 15 |
+
return new NextResponse(JSON.stringify({ error: 'Missing parameter id' }), {
|
| 16 |
+
status: 400,
|
| 17 |
+
headers: {
|
| 18 |
+
'Content-Type': 'application/json',
|
| 19 |
+
...corsHeaders
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const pageNumber = page ? parseInt(page) : 1;
|
| 25 |
+
const personaInfo = await (await sunoApi()).getPersonaPaginated(personaId, pageNumber);
|
| 26 |
+
|
| 27 |
+
return new NextResponse(JSON.stringify(personaInfo), {
|
| 28 |
+
status: 200,
|
| 29 |
+
headers: {
|
| 30 |
+
'Content-Type': 'application/json',
|
| 31 |
+
...corsHeaders
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
} catch (error) {
|
| 35 |
+
console.error('Error fetching persona:', error);
|
| 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: 'GET',
|
| 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/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,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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} requestInterceptor={(req) => {
|
| 13 |
+
// Remove cookies before sending requests
|
| 14 |
+
req.credentials = 'omit';
|
| 15 |
+
console.log(req);
|
| 16 |
+
return req;
|
| 17 |
+
}} />;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export default Swagger;
|
src/app/docs/page.tsx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/generate_stems\`: Make stem tracks (separate audio and music track)
|
| 33 |
+
- \`/api/get_aligned_lyrics\`: Get list of timestamps for each word in the lyrics
|
| 34 |
+
- \`/api/clip\`: Get clip information based on ID passed as query parameter \`id\`
|
| 35 |
+
- \`/api/concat\`: Generate the whole song from extensions
|
| 36 |
+
- \`/api/persona\`: Get persona information and clips based on ID and page number
|
| 37 |
+
\`\`\`
|
| 38 |
+
|
| 39 |
+
Feel free to explore the detailed API parameters and conduct tests on this page.
|
| 40 |
+
`}
|
| 41 |
+
</Markdown>
|
| 42 |
+
</article>
|
| 43 |
+
</Section>
|
| 44 |
+
<Section className="my-10">
|
| 45 |
+
<article className='prose lg:prose-lg max-w-3xl py-10'>
|
| 46 |
+
<h2 className='text-center'>
|
| 47 |
+
Details of the API and testing it online
|
| 48 |
+
</h2>
|
| 49 |
+
<p className='text-red-800 italic'>
|
| 50 |
+
This is just a demo, bound to a test account. Please do not use it frequently, so that more people can test online.
|
| 51 |
+
</p>
|
| 52 |
+
</article>
|
| 53 |
+
|
| 54 |
+
<div className=' border p-4 rounded-2xl shadow-xl hover:shadow-none duration-200'>
|
| 55 |
+
<Swagger spec={spec} />
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
</Section>
|
| 59 |
+
</>
|
| 60 |
+
|
| 61 |
+
);
|
| 62 |
+
}
|
src/app/docs/swagger-suno-api.json
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"negative_tags": {
|
| 142 |
+
"type": "string",
|
| 143 |
+
"description": "Negative Music genre",
|
| 144 |
+
"example": "female edm techno"
|
| 145 |
+
},
|
| 146 |
+
"title": {
|
| 147 |
+
"type": "string",
|
| 148 |
+
"description": "Music title",
|
| 149 |
+
"example": "Silent Battlefield"
|
| 150 |
+
},
|
| 151 |
+
"make_instrumental": {
|
| 152 |
+
"type": "boolean",
|
| 153 |
+
"description": "Whether to generate instrumental music",
|
| 154 |
+
"example": "false"
|
| 155 |
+
},
|
| 156 |
+
"model": {
|
| 157 |
+
"type": "string",
|
| 158 |
+
"description": "Model name ,default is chirp-v3-5",
|
| 159 |
+
"example": "chirp-v3-5|chirp-v3-0"
|
| 160 |
+
},
|
| 161 |
+
"wait_audio": {
|
| 162 |
+
"type": "boolean",
|
| 163 |
+
"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.",
|
| 164 |
+
"example": "false"
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
"responses": {
|
| 172 |
+
"200": {
|
| 173 |
+
"description": "success",
|
| 174 |
+
"content": {
|
| 175 |
+
"application/json": {
|
| 176 |
+
"schema": {
|
| 177 |
+
"type": "array",
|
| 178 |
+
"items": {
|
| 179 |
+
"type": "object",
|
| 180 |
+
"required": ["0", "1"],
|
| 181 |
+
"properties": [
|
| 182 |
+
{
|
| 183 |
+
"$ref": "#/components/schemas/audio_info"
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"$ref": "#/components/schemas/audio_info"
|
| 187 |
+
}
|
| 188 |
+
]
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
},
|
| 197 |
+
"/api/extend_audio": {
|
| 198 |
+
"post": {
|
| 199 |
+
"summary": "Extend audio length.",
|
| 200 |
+
"description": "Extend audio length.",
|
| 201 |
+
"tags": ["default"],
|
| 202 |
+
"requestBody": {
|
| 203 |
+
"content": {
|
| 204 |
+
"application/json": {
|
| 205 |
+
"schema": {
|
| 206 |
+
"type": "object",
|
| 207 |
+
"required": ["audio_id"],
|
| 208 |
+
"properties": {
|
| 209 |
+
"audio_id": {
|
| 210 |
+
"type": "string",
|
| 211 |
+
"description": "The ID of the audio clip to extend.",
|
| 212 |
+
"example": "e76498dc-6ab4-4a10-a19f-8a095790e28d"
|
| 213 |
+
},
|
| 214 |
+
"prompt": {
|
| 215 |
+
"type": "string",
|
| 216 |
+
"description": "Detailed prompt, including information such as music lyrics.",
|
| 217 |
+
"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]"
|
| 218 |
+
},
|
| 219 |
+
"continue_at": {
|
| 220 |
+
"type": "string",
|
| 221 |
+
"description": "Extend a new clip from a song at mm:ss(e.g. 00:30). Default extends from the end of the song.",
|
| 222 |
+
"example": "109.96"
|
| 223 |
+
},
|
| 224 |
+
"title": {
|
| 225 |
+
"type": "string",
|
| 226 |
+
"description": "Music title",
|
| 227 |
+
"example": ""
|
| 228 |
+
},
|
| 229 |
+
"tags": {
|
| 230 |
+
"type": "string",
|
| 231 |
+
"description": "Music genre",
|
| 232 |
+
"example": ""
|
| 233 |
+
},
|
| 234 |
+
"negative_tags": {
|
| 235 |
+
"type": "string",
|
| 236 |
+
"description": "Negative Music genre",
|
| 237 |
+
"example":""
|
| 238 |
+
},
|
| 239 |
+
"model": {
|
| 240 |
+
"type": "string",
|
| 241 |
+
"description": "Model name ,default is chirp-v3-5",
|
| 242 |
+
"example": "chirp-v3-5|chirp-v3-0"
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
},
|
| 251 |
+
"/api/generate_stems": {
|
| 252 |
+
"post": {
|
| 253 |
+
"summary": "Make stem tracks (separate audio and music track).",
|
| 254 |
+
"description": "Make stem tracks (separate audio and music track).",
|
| 255 |
+
"tags": ["default"],
|
| 256 |
+
"requestBody": {
|
| 257 |
+
"content": {
|
| 258 |
+
"application/json": {
|
| 259 |
+
"schema": {
|
| 260 |
+
"type": "object",
|
| 261 |
+
"required": ["audio_id"],
|
| 262 |
+
"properties": {
|
| 263 |
+
"audio_id": {
|
| 264 |
+
"type": "string",
|
| 265 |
+
"description": "The ID of the song to generate stems for.",
|
| 266 |
+
"example": "e76498dc-6ab4-4a10-a19f-8a095790e28d"
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
},
|
| 273 |
+
"responses": {
|
| 274 |
+
"200": {
|
| 275 |
+
"$ref": "#/components/schemas/audio_info"
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
},
|
| 280 |
+
"/api/generate_lyrics": {
|
| 281 |
+
"post": {
|
| 282 |
+
"summary": "Generate lyrics based on Prompt.",
|
| 283 |
+
"description": "Generate lyrics based on Prompt.",
|
| 284 |
+
"tags": ["default"],
|
| 285 |
+
"requestBody": {
|
| 286 |
+
"content": {
|
| 287 |
+
"application/json": {
|
| 288 |
+
"schema": {
|
| 289 |
+
"type": "object",
|
| 290 |
+
"required": ["prompt"],
|
| 291 |
+
"properties": {
|
| 292 |
+
"prompt": {
|
| 293 |
+
"type": "string",
|
| 294 |
+
"description": "Prompt",
|
| 295 |
+
"example": "A soothing lullaby"
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
},
|
| 302 |
+
"responses": {
|
| 303 |
+
"200": {
|
| 304 |
+
"description": "success",
|
| 305 |
+
"content": {
|
| 306 |
+
"application/json": {
|
| 307 |
+
"schema": {
|
| 308 |
+
"type": "object",
|
| 309 |
+
"properties": {
|
| 310 |
+
"text": {
|
| 311 |
+
"type": "string",
|
| 312 |
+
"description": "Lyrics"
|
| 313 |
+
},
|
| 314 |
+
"title": {
|
| 315 |
+
"type": "string",
|
| 316 |
+
"description": "music title"
|
| 317 |
+
},
|
| 318 |
+
"status": {
|
| 319 |
+
"type": "string",
|
| 320 |
+
"description": "Status"
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
},
|
| 330 |
+
"/api/get": {
|
| 331 |
+
"get": {
|
| 332 |
+
"summary": "Get audio information",
|
| 333 |
+
"description": "",
|
| 334 |
+
"tags": ["default"],
|
| 335 |
+
"parameters": [
|
| 336 |
+
{
|
| 337 |
+
"in": "query",
|
| 338 |
+
"name": "ids",
|
| 339 |
+
"description": "Audio IDs, separated by commas. Leave blank to return a list of all music.",
|
| 340 |
+
"required": false,
|
| 341 |
+
"schema": {
|
| 342 |
+
"type": "string"
|
| 343 |
+
}
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
"in": "query",
|
| 347 |
+
"name": "page",
|
| 348 |
+
"description": "Page number",
|
| 349 |
+
"required": false,
|
| 350 |
+
"schema": {
|
| 351 |
+
"type": "number"
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
],
|
| 355 |
+
"responses": {
|
| 356 |
+
"200": {
|
| 357 |
+
"description": "success",
|
| 358 |
+
"content": {
|
| 359 |
+
"application/json": {
|
| 360 |
+
"schema": {
|
| 361 |
+
"type": "array",
|
| 362 |
+
"items": {
|
| 363 |
+
"type": "object",
|
| 364 |
+
"required": ["0", "1"],
|
| 365 |
+
"properties": [
|
| 366 |
+
{
|
| 367 |
+
"$ref": "#/components/schemas/audio_info"
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"$ref": "#/components/schemas/audio_info"
|
| 371 |
+
}
|
| 372 |
+
]
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
},
|
| 381 |
+
"/api/get_limit": {
|
| 382 |
+
"get": {
|
| 383 |
+
"summary": "Get quota information.",
|
| 384 |
+
"description": "",
|
| 385 |
+
"tags": ["default"],
|
| 386 |
+
"responses": {
|
| 387 |
+
"200": {
|
| 388 |
+
"description": "success",
|
| 389 |
+
"content": {
|
| 390 |
+
"application/json": {
|
| 391 |
+
"schema": {
|
| 392 |
+
"type": "object",
|
| 393 |
+
"required": [
|
| 394 |
+
"credits_left",
|
| 395 |
+
"period",
|
| 396 |
+
"monthly_limit",
|
| 397 |
+
"monthly_usage"
|
| 398 |
+
],
|
| 399 |
+
"properties": {
|
| 400 |
+
"credits_left": {
|
| 401 |
+
"type": "number",
|
| 402 |
+
"description": "Remaining credits,Each generated audio consumes 5 credits."
|
| 403 |
+
},
|
| 404 |
+
"period": {
|
| 405 |
+
"type": "string",
|
| 406 |
+
"description": "Period"
|
| 407 |
+
},
|
| 408 |
+
"monthly_limit": {
|
| 409 |
+
"type": "number",
|
| 410 |
+
"description": "Monthly limit"
|
| 411 |
+
},
|
| 412 |
+
"monthly_usage": {
|
| 413 |
+
"type": "number",
|
| 414 |
+
"description": "Monthly usage"
|
| 415 |
+
}
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
}
|
| 422 |
+
}
|
| 423 |
+
},
|
| 424 |
+
"/api/get_aligned_lyrics": {
|
| 425 |
+
"get": {
|
| 426 |
+
"summary": "Get lyric alignment.",
|
| 427 |
+
"description": "Get lyric alignment.",
|
| 428 |
+
"tags": ["default"],
|
| 429 |
+
"parameters": [
|
| 430 |
+
{
|
| 431 |
+
"name": "song_id",
|
| 432 |
+
"in": "query",
|
| 433 |
+
"required": true,
|
| 434 |
+
"description": "Song ID",
|
| 435 |
+
"schema": {
|
| 436 |
+
"type": "string"
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
],
|
| 440 |
+
"responses": {
|
| 441 |
+
"200": {
|
| 442 |
+
"$ref": "#/components/schemas/audio_info"
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
+
},
|
| 447 |
+
"/api/clip": {
|
| 448 |
+
"get": {
|
| 449 |
+
"summary": "Get clip information based on ID.",
|
| 450 |
+
"description": "Retrieve specific clip information using the provided clip ID as a query parameter.",
|
| 451 |
+
"tags": ["default"],
|
| 452 |
+
"parameters": [
|
| 453 |
+
{
|
| 454 |
+
"name": "id",
|
| 455 |
+
"in": "query",
|
| 456 |
+
"required": true,
|
| 457 |
+
"description": "Clip ID",
|
| 458 |
+
"schema": {
|
| 459 |
+
"type": "string"
|
| 460 |
+
}
|
| 461 |
+
}
|
| 462 |
+
],
|
| 463 |
+
"responses": {
|
| 464 |
+
"200": {
|
| 465 |
+
"description": "success",
|
| 466 |
+
"content": {
|
| 467 |
+
"application/json": {
|
| 468 |
+
"schema": {
|
| 469 |
+
"$ref": "#/components/schemas/audio_info"
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
}
|
| 473 |
+
},
|
| 474 |
+
"400": {
|
| 475 |
+
"description": "Missing parameter id",
|
| 476 |
+
"content": {
|
| 477 |
+
"application/json": {
|
| 478 |
+
"schema": {
|
| 479 |
+
"type": "object",
|
| 480 |
+
"properties": {
|
| 481 |
+
"error": {
|
| 482 |
+
"type": "string",
|
| 483 |
+
"example": "Missing parameter id"
|
| 484 |
+
}
|
| 485 |
+
}
|
| 486 |
+
}
|
| 487 |
+
}
|
| 488 |
+
}
|
| 489 |
+
},
|
| 490 |
+
"500": {
|
| 491 |
+
"description": "Internal server error",
|
| 492 |
+
"content": {
|
| 493 |
+
"application/json": {
|
| 494 |
+
"schema": {
|
| 495 |
+
"type": "object",
|
| 496 |
+
"properties": {
|
| 497 |
+
"error": {
|
| 498 |
+
"type": "string",
|
| 499 |
+
"example": "Internal server error"
|
| 500 |
+
}
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
}
|
| 508 |
+
},
|
| 509 |
+
"/api/concat": {
|
| 510 |
+
"post": {
|
| 511 |
+
"summary": "Generate the whole song from extensions.",
|
| 512 |
+
"description": "Concatenate audio clips to generate a complete song using the provided clip ID.",
|
| 513 |
+
"tags": ["default"],
|
| 514 |
+
"requestBody": {
|
| 515 |
+
"content": {
|
| 516 |
+
"application/json": {
|
| 517 |
+
"schema": {
|
| 518 |
+
"type": "object",
|
| 519 |
+
"required": ["clip_id"],
|
| 520 |
+
"properties": {
|
| 521 |
+
"clip_id": {
|
| 522 |
+
"type": "string",
|
| 523 |
+
"description": "Clip ID"
|
| 524 |
+
}
|
| 525 |
+
}
|
| 526 |
+
}
|
| 527 |
+
}
|
| 528 |
+
}
|
| 529 |
+
},
|
| 530 |
+
"responses": {
|
| 531 |
+
"200": {
|
| 532 |
+
"description": "success",
|
| 533 |
+
"content": {
|
| 534 |
+
"application/json": {
|
| 535 |
+
"schema": {
|
| 536 |
+
"$ref": "#/components/schemas/audio_info"
|
| 537 |
+
}
|
| 538 |
+
}
|
| 539 |
+
}
|
| 540 |
+
},
|
| 541 |
+
"400": {
|
| 542 |
+
"description": "Clip id is required",
|
| 543 |
+
"content": {
|
| 544 |
+
"application/json": {
|
| 545 |
+
"schema": {
|
| 546 |
+
"type": "object",
|
| 547 |
+
"properties": {
|
| 548 |
+
"error": {
|
| 549 |
+
"type": "string",
|
| 550 |
+
"example": "Clip id is required"
|
| 551 |
+
}
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
}
|
| 555 |
+
}
|
| 556 |
+
},
|
| 557 |
+
"402": {
|
| 558 |
+
"description": "Payment required",
|
| 559 |
+
"content": {
|
| 560 |
+
"application/json": {
|
| 561 |
+
"schema": {
|
| 562 |
+
"type": "object",
|
| 563 |
+
"properties": {
|
| 564 |
+
"error": {
|
| 565 |
+
"type": "string",
|
| 566 |
+
"example": "Payment required"
|
| 567 |
+
}
|
| 568 |
+
}
|
| 569 |
+
}
|
| 570 |
+
}
|
| 571 |
+
}
|
| 572 |
+
},
|
| 573 |
+
"500": {
|
| 574 |
+
"description": "Internal server error",
|
| 575 |
+
"content": {
|
| 576 |
+
"application/json": {
|
| 577 |
+
"schema": {
|
| 578 |
+
"type": "object",
|
| 579 |
+
"properties": {
|
| 580 |
+
"error": {
|
| 581 |
+
"type": "string",
|
| 582 |
+
"example": "Internal server error"
|
| 583 |
+
}
|
| 584 |
+
}
|
| 585 |
+
}
|
| 586 |
+
}
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
}
|
| 590 |
+
}
|
| 591 |
+
},
|
| 592 |
+
"/api/persona": {
|
| 593 |
+
"get": {
|
| 594 |
+
"summary": "Get persona information and clips.",
|
| 595 |
+
"description": "Retrieve persona information, including associated clips and pagination data.",
|
| 596 |
+
"tags": ["default"],
|
| 597 |
+
"parameters": [
|
| 598 |
+
{
|
| 599 |
+
"name": "id",
|
| 600 |
+
"in": "query",
|
| 601 |
+
"required": true,
|
| 602 |
+
"description": "Persona ID",
|
| 603 |
+
"schema": {
|
| 604 |
+
"type": "string"
|
| 605 |
+
}
|
| 606 |
+
},
|
| 607 |
+
{
|
| 608 |
+
"name": "page",
|
| 609 |
+
"in": "query",
|
| 610 |
+
"required": false,
|
| 611 |
+
"description": "Page number (defaults to 1)",
|
| 612 |
+
"schema": {
|
| 613 |
+
"type": "integer",
|
| 614 |
+
"default": 1
|
| 615 |
+
}
|
| 616 |
+
}
|
| 617 |
+
],
|
| 618 |
+
"responses": {
|
| 619 |
+
"200": {
|
| 620 |
+
"description": "success",
|
| 621 |
+
"content": {
|
| 622 |
+
"application/json": {
|
| 623 |
+
"schema": {
|
| 624 |
+
"type": "object",
|
| 625 |
+
"properties": {
|
| 626 |
+
"persona": {
|
| 627 |
+
"type": "object",
|
| 628 |
+
"properties": {
|
| 629 |
+
"id": {
|
| 630 |
+
"type": "string",
|
| 631 |
+
"description": "Persona ID"
|
| 632 |
+
},
|
| 633 |
+
"name": {
|
| 634 |
+
"type": "string",
|
| 635 |
+
"description": "Persona name"
|
| 636 |
+
},
|
| 637 |
+
"description": {
|
| 638 |
+
"type": "string",
|
| 639 |
+
"description": "Persona description"
|
| 640 |
+
},
|
| 641 |
+
"image_s3_id": {
|
| 642 |
+
"type": "string",
|
| 643 |
+
"description": "Persona image URL"
|
| 644 |
+
},
|
| 645 |
+
"root_clip_id": {
|
| 646 |
+
"type": "string",
|
| 647 |
+
"description": "Root clip ID"
|
| 648 |
+
},
|
| 649 |
+
"clip": {
|
| 650 |
+
"type": "object",
|
| 651 |
+
"description": "Root clip information"
|
| 652 |
+
},
|
| 653 |
+
"persona_clips": {
|
| 654 |
+
"type": "array",
|
| 655 |
+
"items": {
|
| 656 |
+
"type": "object",
|
| 657 |
+
"properties": {
|
| 658 |
+
"clip": {
|
| 659 |
+
"type": "object",
|
| 660 |
+
"description": "Clip information"
|
| 661 |
+
}
|
| 662 |
+
}
|
| 663 |
+
}
|
| 664 |
+
},
|
| 665 |
+
"is_suno_persona": {
|
| 666 |
+
"type": "boolean",
|
| 667 |
+
"description": "Whether this is a Suno official persona"
|
| 668 |
+
},
|
| 669 |
+
"is_public": {
|
| 670 |
+
"type": "boolean",
|
| 671 |
+
"description": "Whether this persona is public"
|
| 672 |
+
},
|
| 673 |
+
"upvote_count": {
|
| 674 |
+
"type": "integer",
|
| 675 |
+
"description": "Number of upvotes"
|
| 676 |
+
},
|
| 677 |
+
"clip_count": {
|
| 678 |
+
"type": "integer",
|
| 679 |
+
"description": "Number of clips"
|
| 680 |
+
}
|
| 681 |
+
}
|
| 682 |
+
},
|
| 683 |
+
"total_results": {
|
| 684 |
+
"type": "integer",
|
| 685 |
+
"description": "Total number of results"
|
| 686 |
+
},
|
| 687 |
+
"current_page": {
|
| 688 |
+
"type": "integer",
|
| 689 |
+
"description": "Current page number"
|
| 690 |
+
},
|
| 691 |
+
"is_following": {
|
| 692 |
+
"type": "boolean",
|
| 693 |
+
"description": "Whether the current user is following this persona"
|
| 694 |
+
}
|
| 695 |
+
}
|
| 696 |
+
}
|
| 697 |
+
}
|
| 698 |
+
}
|
| 699 |
+
},
|
| 700 |
+
"400": {
|
| 701 |
+
"description": "Missing parameter id",
|
| 702 |
+
"content": {
|
| 703 |
+
"application/json": {
|
| 704 |
+
"schema": {
|
| 705 |
+
"type": "object",
|
| 706 |
+
"properties": {
|
| 707 |
+
"error": {
|
| 708 |
+
"type": "string",
|
| 709 |
+
"example": "Missing parameter id"
|
| 710 |
+
}
|
| 711 |
+
}
|
| 712 |
+
}
|
| 713 |
+
}
|
| 714 |
+
}
|
| 715 |
+
},
|
| 716 |
+
"500": {
|
| 717 |
+
"description": "Internal server error",
|
| 718 |
+
"content": {
|
| 719 |
+
"application/json": {
|
| 720 |
+
"schema": {
|
| 721 |
+
"type": "object",
|
| 722 |
+
"properties": {
|
| 723 |
+
"error": {
|
| 724 |
+
"type": "string",
|
| 725 |
+
"example": "Internal server error"
|
| 726 |
+
}
|
| 727 |
+
}
|
| 728 |
+
}
|
| 729 |
+
}
|
| 730 |
+
}
|
| 731 |
+
}
|
| 732 |
+
}
|
| 733 |
+
}
|
| 734 |
+
}
|
| 735 |
+
},
|
| 736 |
+
"components": {
|
| 737 |
+
"schemas": {
|
| 738 |
+
"audio_info": {
|
| 739 |
+
"type": "object",
|
| 740 |
+
"required": [
|
| 741 |
+
"id",
|
| 742 |
+
"title",
|
| 743 |
+
"image_url",
|
| 744 |
+
"lyric",
|
| 745 |
+
"audio_url",
|
| 746 |
+
"video_url",
|
| 747 |
+
"created_at",
|
| 748 |
+
"model_name",
|
| 749 |
+
"status",
|
| 750 |
+
"gpt_description_prompt",
|
| 751 |
+
"prompt",
|
| 752 |
+
"type",
|
| 753 |
+
"tags"
|
| 754 |
+
],
|
| 755 |
+
"properties": {
|
| 756 |
+
"id": {
|
| 757 |
+
"type": "string",
|
| 758 |
+
"description": "audio id"
|
| 759 |
+
},
|
| 760 |
+
"title": {
|
| 761 |
+
"type": "string",
|
| 762 |
+
"description": "music title"
|
| 763 |
+
},
|
| 764 |
+
"image_url": {
|
| 765 |
+
"type": "string",
|
| 766 |
+
"description": "music cover image"
|
| 767 |
+
},
|
| 768 |
+
"lyric": {
|
| 769 |
+
"type": "string",
|
| 770 |
+
"description": "music lyric"
|
| 771 |
+
},
|
| 772 |
+
"audio_url": {
|
| 773 |
+
"type": "string",
|
| 774 |
+
"description": "music download url"
|
| 775 |
+
},
|
| 776 |
+
"video_url": {
|
| 777 |
+
"type": "string",
|
| 778 |
+
"description": "Music video download link, can be used to share"
|
| 779 |
+
},
|
| 780 |
+
"created_at": {
|
| 781 |
+
"type": "string",
|
| 782 |
+
"description": "Create time"
|
| 783 |
+
},
|
| 784 |
+
"model_name": {
|
| 785 |
+
"type": "string",
|
| 786 |
+
"description": "suno model name, chirp-v3"
|
| 787 |
+
},
|
| 788 |
+
"status": {
|
| 789 |
+
"type": "string",
|
| 790 |
+
"description": "The generated states include submitted, queue, streaming, complete."
|
| 791 |
+
},
|
| 792 |
+
"gpt_description_prompt": {
|
| 793 |
+
"type": "string",
|
| 794 |
+
"description": "Simple mode on user input prompt, Suno will generate formal prompts, lyrics, etc."
|
| 795 |
+
},
|
| 796 |
+
"prompt": {
|
| 797 |
+
"type": "string",
|
| 798 |
+
"description": "The final prompt for executing the generation task, customized by the user in custom mode, automatically generated by Suno in simple mode."
|
| 799 |
+
},
|
| 800 |
+
"type": {
|
| 801 |
+
"type": "string",
|
| 802 |
+
"description": "Type"
|
| 803 |
+
},
|
| 804 |
+
"tags": {
|
| 805 |
+
"type": "string",
|
| 806 |
+
"description": "Music genre. User-provided in custom mode, automatically generated by Suno in simple mode."
|
| 807 |
+
}
|
| 808 |
+
},
|
| 809 |
+
"title": "audio_info",
|
| 810 |
+
"description": "Audio Info"
|
| 811 |
+
}
|
| 812 |
+
}
|
| 813 |
+
}
|
| 814 |
+
}
|
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,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Section from "./components/Section";
|
| 2 |
+
import Markdown from 'react-markdown';
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
export default function Home() {
|
| 6 |
+
|
| 7 |
+
const markdown = `
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
## 👋 Introduction
|
| 11 |
+
|
| 12 |
+
Suno.ai v3 is an amazing AI music service. Although the official API is not yet available, we couldn't wait to integrate its capabilities somewhere.
|
| 13 |
+
|
| 14 |
+
We discovered that some users have similar needs, so we decided to open-source this project, hoping you'll like it.
|
| 15 |
+
|
| 16 |
+
We update quickly, please star us on Github: [github.com/gcui-art/suno-api](https://github.com/gcui-art/suno-api) ⭐
|
| 17 |
+
|
| 18 |
+
## 🌟 Features
|
| 19 |
+
|
| 20 |
+
- Perfectly implements the creation API from \`app.suno.ai\`
|
| 21 |
+
- Compatible with the format of OpenAI’s \`/v1/chat/completions\` API.
|
| 22 |
+
- Automatically keep the account active.
|
| 23 |
+
- Supports \`Custom Mode\`
|
| 24 |
+
- One-click deployment to Vercel
|
| 25 |
+
- In addition to the standard API, it also adapts to the API Schema of Agent platforms like GPTs and Coze, so you can use it as a tool/plugin/Action for LLMs and integrate it into any AI Agent.
|
| 26 |
+
- Permissive open-source license, allowing you to freely integrate and modify.
|
| 27 |
+
|
| 28 |
+
## 🚀 Getting Started
|
| 29 |
+
|
| 30 |
+
### 1. Obtain the cookie of your app.suno.ai account
|
| 31 |
+
|
| 32 |
+
1. Head over to [app.suno.ai](https://app.suno.ai) using your browser.
|
| 33 |
+
2. Open up the browser console: hit \`F12\` or access the \`Developer Tools\`.
|
| 34 |
+
3. Navigate to the \`Network tab\`.
|
| 35 |
+
4. Give the page a quick refresh.
|
| 36 |
+
5. Identify the request that includes the keyword \`client?_clerk_js_version\`.
|
| 37 |
+
6. Click on it and switch over to the \`Header\` tab.
|
| 38 |
+
7. Locate the \`Cookie\` section, hover your mouse over it, and copy the value of the Cookie.
|
| 39 |
+
`;
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
const markdown_part2 = `
|
| 43 |
+
### 2. Clone and deploy this project
|
| 44 |
+
|
| 45 |
+
You can choose your preferred deployment method:
|
| 46 |
+
|
| 47 |
+
#### Deploy to Vercel
|
| 48 |
+
|
| 49 |
+
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api)
|
| 50 |
+
|
| 51 |
+
#### Run locally
|
| 52 |
+
|
| 53 |
+
\`\`\`bash
|
| 54 |
+
git clone https://github.com/gcui-art/suno-api.git
|
| 55 |
+
cd suno-api
|
| 56 |
+
npm install
|
| 57 |
+
\`\`\`
|
| 58 |
+
|
| 59 |
+
### 3. Configure suno-api
|
| 60 |
+
|
| 61 |
+
- If deployed to Vercel, please add an environment variable \`SUNO_COOKIE\` in the Vercel dashboard, with the value of the cookie obtained in the first step.
|
| 62 |
+
|
| 63 |
+
- If you’re running this locally, be sure to add the following to your \`.env\` file:
|
| 64 |
+
|
| 65 |
+
\`\`\`bash
|
| 66 |
+
SUNO_COOKIE=<your-cookie>
|
| 67 |
+
\`\`\`
|
| 68 |
+
|
| 69 |
+
### 4. Run suno-api
|
| 70 |
+
|
| 71 |
+
- If you’ve deployed to Vercel:
|
| 72 |
+
- Please click on Deploy in the Vercel dashboard and wait for the deployment to be successful.
|
| 73 |
+
- Visit the \`https://<vercel-assigned-domain>/api/get_limit\` API for testing.
|
| 74 |
+
- If running locally:
|
| 75 |
+
- Run \`npm run dev\`.
|
| 76 |
+
- Visit the \`http://localhost:3000/api/get_limit\` API for testing.
|
| 77 |
+
- If the following result is returned:
|
| 78 |
+
|
| 79 |
+
\`\`\`json
|
| 80 |
+
{
|
| 81 |
+
"credits_left": 50,
|
| 82 |
+
"period": "day",
|
| 83 |
+
"monthly_limit": 50,
|
| 84 |
+
"monthly_usage": 50
|
| 85 |
+
}
|
| 86 |
+
\`\`\`
|
| 87 |
+
|
| 88 |
+
it means the program is running normally.
|
| 89 |
+
|
| 90 |
+
### 5. Use Suno API
|
| 91 |
+
|
| 92 |
+
You can check out the detailed API documentation at [suno.gcui.ai/docs](https://suno.gcui.ai/docs).
|
| 93 |
+
|
| 94 |
+
## 📚 API Reference
|
| 95 |
+
|
| 96 |
+
Suno API currently mainly implements the following APIs:
|
| 97 |
+
|
| 98 |
+
\`\`\`bash
|
| 99 |
+
- \`/api/generate\`: Generate music
|
| 100 |
+
- \`/v1/chat/completions\`: Generate music - Call the generate API in a format
|
| 101 |
+
that works with OpenAI’s API.
|
| 102 |
+
- \`/api/custom_generate\`: Generate music (Custom Mode, support setting lyrics,
|
| 103 |
+
music style, title, etc.)
|
| 104 |
+
- \`/api/generate_lyrics\`: Generate lyrics based on prompt
|
| 105 |
+
- \`/api/get\`: Get music list
|
| 106 |
+
- \`/api/get?ids=\`: Get music Info by id, separate multiple id with ",".
|
| 107 |
+
- \`/api/get_limit\`: Get quota Info
|
| 108 |
+
- \`/api/extend_audio\`: Extend audio length
|
| 109 |
+
- \`/api/generate_stems\`: Make stem tracks (separate audio and music track)
|
| 110 |
+
- \`/api/get_aligned_lyrics\`: Get list of timestamps for each word in the lyrics
|
| 111 |
+
- \`/api/concat\`: Generate the whole song from extensions
|
| 112 |
+
\`\`\`
|
| 113 |
+
|
| 114 |
+
For more detailed documentation, please check out the demo site:
|
| 115 |
+
|
| 116 |
+
👉 [suno.gcui.ai/docs](https://suno.gcui.ai/docs)
|
| 117 |
+
|
| 118 |
+
`;
|
| 119 |
+
return (
|
| 120 |
+
<>
|
| 121 |
+
<Section className="">
|
| 122 |
+
<div className="flex flex-col m-auto py-20 text-center items-center justify-center gap-4 my-8
|
| 123 |
+
lg:px-20 px-4
|
| 124 |
+
bg-indigo-900/90 rounded-2xl border shadow-2xl hover:shadow-none duration-200">
|
| 125 |
+
<span className=" px-5 py-1 text-xs font-light border rounded-full
|
| 126 |
+
border-white/20 uppercase text-white/50">
|
| 127 |
+
Unofficial
|
| 128 |
+
</span>
|
| 129 |
+
<h1 className="font-bold text-7xl flex text-white/90">
|
| 130 |
+
Suno AI API
|
| 131 |
+
</h1>
|
| 132 |
+
<p className="text-white/80 text-lg">
|
| 133 |
+
`Suno-api` is an open-source project that enables you to set up your own Suno AI API.
|
| 134 |
+
</p>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
</Section>
|
| 138 |
+
<Section className="my-10">
|
| 139 |
+
<article className="prose lg:prose-lg max-w-3xl">
|
| 140 |
+
<Markdown>
|
| 141 |
+
{markdown}
|
| 142 |
+
</Markdown>
|
| 143 |
+
<video controls width="1024" className="w-full border rounded-lg shadow-xl">
|
| 144 |
+
<source src="/get-cookie-demo.mp4" type="video/mp4" />
|
| 145 |
+
Your browser does not support frames.
|
| 146 |
+
</video>
|
| 147 |
+
<Markdown>
|
| 148 |
+
{markdown_part2}
|
| 149 |
+
</Markdown>
|
| 150 |
+
</article>
|
| 151 |
+
</Section>
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
</>
|
| 155 |
+
);
|
| 156 |
+
}
|
src/app/v1/chat/completions/route.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse, NextRequest } from "next/server";
|
| 2 |
+
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
|
| 3 |
+
import { corsHeaders } from "@/lib/utils";
|
| 4 |
+
|
| 5 |
+
export const dynamic = "force-dynamic";
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* desc
|
| 9 |
+
*
|
| 10 |
+
*/
|
| 11 |
+
export async function POST(req: NextRequest) {
|
| 12 |
+
try {
|
| 13 |
+
|
| 14 |
+
const body = await req.json();
|
| 15 |
+
|
| 16 |
+
let userMessage = null;
|
| 17 |
+
const { messages } = body;
|
| 18 |
+
for (let message of messages) {
|
| 19 |
+
if (message.role == 'user') {
|
| 20 |
+
userMessage = message;
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
if (!userMessage) {
|
| 25 |
+
return new NextResponse(JSON.stringify({ error: 'Prompt message is required' }), {
|
| 26 |
+
status: 400,
|
| 27 |
+
headers: {
|
| 28 |
+
'Content-Type': 'application/json',
|
| 29 |
+
...corsHeaders
|
| 30 |
+
}
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
const audioInfo = await (await sunoApi()).generate(userMessage.content, true, DEFAULT_MODEL, true);
|
| 36 |
+
|
| 37 |
+
const audio = audioInfo[0]
|
| 38 |
+
const data = `## Song Title: ${audio.title}\n\n### Lyrics:\n${audio.lyric}\n### Listen to the song: ${audio.audio_url}`
|
| 39 |
+
|
| 40 |
+
return new NextResponse(data, {
|
| 41 |
+
status: 200,
|
| 42 |
+
headers: corsHeaders
|
| 43 |
+
});
|
| 44 |
+
} catch (error: any) {
|
| 45 |
+
console.error('Error generating audio:', JSON.stringify(error.response.data));
|
| 46 |
+
return new NextResponse(JSON.stringify({ error: 'Internal server error: ' + JSON.stringify(error.response.data.detail) }), {
|
| 47 |
+
status: 500,
|
| 48 |
+
headers: {
|
| 49 |
+
'Content-Type': 'application/json',
|
| 50 |
+
...corsHeaders
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export async function OPTIONS(request: Request) {
|
| 57 |
+
return new Response(null, {
|
| 58 |
+
status: 200,
|
| 59 |
+
headers: corsHeaders
|
| 60 |
+
});
|
| 61 |
+
}
|
src/lib/SunoApi.ts
ADDED
|
@@ -0,0 +1,870 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios, { AxiosInstance } from 'axios';
|
| 2 |
+
import UserAgent from 'user-agents';
|
| 3 |
+
import pino from 'pino';
|
| 4 |
+
import yn from 'yn';
|
| 5 |
+
import { isPage, sleep, waitForRequests } from '@/lib/utils';
|
| 6 |
+
import * as cookie from 'cookie';
|
| 7 |
+
import { randomUUID } from 'node:crypto';
|
| 8 |
+
import { Solver } from '@2captcha/captcha-solver';
|
| 9 |
+
import { paramsCoordinates } from '@2captcha/captcha-solver/dist/structs/2captcha';
|
| 10 |
+
import { BrowserContext, Page, Locator, chromium, firefox } from 'rebrowser-playwright-core';
|
| 11 |
+
import { createCursor, Cursor } from 'ghost-cursor-playwright';
|
| 12 |
+
import { promises as fs } from 'fs';
|
| 13 |
+
import path from 'node:path';
|
| 14 |
+
|
| 15 |
+
// sunoApi instance caching
|
| 16 |
+
const globalForSunoApi = global as unknown as { sunoApiCache?: Map<string, SunoApi> };
|
| 17 |
+
const cache = globalForSunoApi.sunoApiCache || new Map<string, SunoApi>();
|
| 18 |
+
globalForSunoApi.sunoApiCache = cache;
|
| 19 |
+
|
| 20 |
+
const logger = pino();
|
| 21 |
+
export const DEFAULT_MODEL = 'chirp-v3-5';
|
| 22 |
+
|
| 23 |
+
export interface AudioInfo {
|
| 24 |
+
id: string; // Unique identifier for the audio
|
| 25 |
+
title?: string; // Title of the audio
|
| 26 |
+
image_url?: string; // URL of the image associated with the audio
|
| 27 |
+
lyric?: string; // Lyrics of the audio
|
| 28 |
+
audio_url?: string; // URL of the audio file
|
| 29 |
+
video_url?: string; // URL of the video associated with the audio
|
| 30 |
+
created_at: string; // Date and time when the audio was created
|
| 31 |
+
model_name: string; // Name of the model used for audio generation
|
| 32 |
+
gpt_description_prompt?: string; // Prompt for GPT description
|
| 33 |
+
prompt?: string; // Prompt for audio generation
|
| 34 |
+
status: string; // Status
|
| 35 |
+
type?: string;
|
| 36 |
+
tags?: string; // Genre of music.
|
| 37 |
+
negative_tags?: string; // Negative tags of music.
|
| 38 |
+
duration?: string; // Duration of the audio
|
| 39 |
+
error_message?: string; // Error message if any
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
interface PersonaResponse {
|
| 43 |
+
persona: {
|
| 44 |
+
id: string;
|
| 45 |
+
name: string;
|
| 46 |
+
description: string;
|
| 47 |
+
image_s3_id: string;
|
| 48 |
+
root_clip_id: string;
|
| 49 |
+
clip: any; // You can define a more specific type if needed
|
| 50 |
+
user_display_name: string;
|
| 51 |
+
user_handle: string;
|
| 52 |
+
user_image_url: string;
|
| 53 |
+
persona_clips: Array<{
|
| 54 |
+
clip: any; // You can define a more specific type if needed
|
| 55 |
+
}>;
|
| 56 |
+
is_suno_persona: boolean;
|
| 57 |
+
is_trashed: boolean;
|
| 58 |
+
is_owned: boolean;
|
| 59 |
+
is_public: boolean;
|
| 60 |
+
is_public_approved: boolean;
|
| 61 |
+
is_loved: boolean;
|
| 62 |
+
upvote_count: number;
|
| 63 |
+
clip_count: number;
|
| 64 |
+
};
|
| 65 |
+
total_results: number;
|
| 66 |
+
current_page: number;
|
| 67 |
+
is_following: boolean;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
class SunoApi {
|
| 71 |
+
private static BASE_URL: string = 'https://studio-api.prod.suno.com';
|
| 72 |
+
private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
|
| 73 |
+
private static CLERK_VERSION = '5.15.0';
|
| 74 |
+
|
| 75 |
+
private readonly client: AxiosInstance;
|
| 76 |
+
private sid?: string;
|
| 77 |
+
private currentToken?: string;
|
| 78 |
+
private deviceId?: string;
|
| 79 |
+
private userAgent?: string;
|
| 80 |
+
private cookies: Record<string, string | undefined>;
|
| 81 |
+
private solver = new Solver(process.env.TWOCAPTCHA_KEY + '');
|
| 82 |
+
private ghostCursorEnabled = yn(process.env.BROWSER_GHOST_CURSOR, { default: false });
|
| 83 |
+
private cursor?: Cursor;
|
| 84 |
+
|
| 85 |
+
constructor(cookies: string) {
|
| 86 |
+
this.userAgent = new UserAgent(/Macintosh/).random().toString(); // Usually Mac systems get less amount of CAPTCHAs
|
| 87 |
+
this.cookies = cookie.parse(cookies);
|
| 88 |
+
this.deviceId = this.cookies.ajs_anonymous_id || randomUUID();
|
| 89 |
+
this.client = axios.create({
|
| 90 |
+
withCredentials: true,
|
| 91 |
+
headers: {
|
| 92 |
+
'Affiliate-Id': 'undefined',
|
| 93 |
+
'Device-Id': `"${this.deviceId}"`,
|
| 94 |
+
'x-suno-client': 'Android prerelease-4nt180t 1.0.42',
|
| 95 |
+
'X-Requested-With': 'com.suno.android',
|
| 96 |
+
'sec-ch-ua': '"Chromium";v="130", "Android WebView";v="130", "Not?A_Brand";v="99"',
|
| 97 |
+
'sec-ch-ua-mobile': '?1',
|
| 98 |
+
'sec-ch-ua-platform': '"Android"',
|
| 99 |
+
'User-Agent': this.userAgent
|
| 100 |
+
}
|
| 101 |
+
});
|
| 102 |
+
this.client.interceptors.request.use(config => {
|
| 103 |
+
if (this.currentToken && !config.headers.Authorization)
|
| 104 |
+
config.headers.Authorization = `Bearer ${this.currentToken}`;
|
| 105 |
+
const cookiesArray = Object.entries(this.cookies).map(([key, value]) =>
|
| 106 |
+
cookie.serialize(key, value as string)
|
| 107 |
+
);
|
| 108 |
+
config.headers.Cookie = cookiesArray.join('; ');
|
| 109 |
+
return config;
|
| 110 |
+
});
|
| 111 |
+
this.client.interceptors.response.use(resp => {
|
| 112 |
+
const setCookieHeader = resp.headers['set-cookie'];
|
| 113 |
+
if (Array.isArray(setCookieHeader)) {
|
| 114 |
+
const newCookies = cookie.parse(setCookieHeader.join('; '));
|
| 115 |
+
for (const [key, value] of Object.entries(newCookies)) {
|
| 116 |
+
this.cookies[key] = value;
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
return resp;
|
| 120 |
+
})
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
public async init(): Promise<SunoApi> {
|
| 124 |
+
//await this.getClerkLatestVersion();
|
| 125 |
+
await this.getAuthToken();
|
| 126 |
+
await this.keepAlive();
|
| 127 |
+
return this;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/**
|
| 131 |
+
* Get the clerk package latest version id.
|
| 132 |
+
* This method is commented because we are now using a hard-coded Clerk version, hence this method is not needed.
|
| 133 |
+
|
| 134 |
+
private async getClerkLatestVersion() {
|
| 135 |
+
// URL to get clerk version ID
|
| 136 |
+
const getClerkVersionUrl = `${SunoApi.JSDELIVR_BASE_URL}/v1/package/npm/@clerk/clerk-js`;
|
| 137 |
+
// Get clerk version ID
|
| 138 |
+
const versionListResponse = await this.client.get(getClerkVersionUrl);
|
| 139 |
+
if (!versionListResponse?.data?.['tags']['latest']) {
|
| 140 |
+
throw new Error(
|
| 141 |
+
'Failed to get clerk version info, Please try again later'
|
| 142 |
+
);
|
| 143 |
+
}
|
| 144 |
+
// Save clerk version ID for auth
|
| 145 |
+
SunoApi.clerkVersion = versionListResponse?.data?.['tags']['latest'];
|
| 146 |
+
}
|
| 147 |
+
*/
|
| 148 |
+
|
| 149 |
+
/**
|
| 150 |
+
* Get the session ID and save it for later use.
|
| 151 |
+
*/
|
| 152 |
+
private async getAuthToken() {
|
| 153 |
+
logger.info('Getting the session ID');
|
| 154 |
+
// URL to get session ID
|
| 155 |
+
const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_is_native=true&_clerk_js_version=${SunoApi.CLERK_VERSION}`;
|
| 156 |
+
// Get session ID
|
| 157 |
+
const sessionResponse = await this.client.get(getSessionUrl, {
|
| 158 |
+
headers: { Authorization: this.cookies.__client }
|
| 159 |
+
});
|
| 160 |
+
if (!sessionResponse?.data?.response?.last_active_session_id) {
|
| 161 |
+
throw new Error(
|
| 162 |
+
'Failed to get session id, you may need to update the SUNO_COOKIE'
|
| 163 |
+
);
|
| 164 |
+
}
|
| 165 |
+
// Save session ID for later use
|
| 166 |
+
this.sid = sessionResponse.data.response.last_active_session_id;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/**
|
| 170 |
+
* Keep the session alive.
|
| 171 |
+
* @param isWait Indicates if the method should wait for the session to be fully renewed before returning.
|
| 172 |
+
*/
|
| 173 |
+
public async keepAlive(isWait?: boolean): Promise<void> {
|
| 174 |
+
if (!this.sid) {
|
| 175 |
+
throw new Error('Session ID is not set. Cannot renew token.');
|
| 176 |
+
}
|
| 177 |
+
// URL to renew session token
|
| 178 |
+
const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens?_is_native=true&_clerk_js_version=${SunoApi.CLERK_VERSION}`;
|
| 179 |
+
// Renew session token
|
| 180 |
+
logger.info('KeepAlive...\n');
|
| 181 |
+
const renewResponse = await this.client.post(renewUrl, {}, {
|
| 182 |
+
headers: { Authorization: this.cookies.__client }
|
| 183 |
+
});
|
| 184 |
+
if (isWait) {
|
| 185 |
+
await sleep(1, 2);
|
| 186 |
+
}
|
| 187 |
+
const newToken = renewResponse.data.jwt;
|
| 188 |
+
// Update Authorization field in request header with the new JWT token
|
| 189 |
+
this.currentToken = newToken;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
/**
|
| 193 |
+
* Get the session token (not to be confused with session ID) and save it for later use.
|
| 194 |
+
*/
|
| 195 |
+
private async getSessionToken() {
|
| 196 |
+
const tokenResponse = await this.client.post(
|
| 197 |
+
`${SunoApi.BASE_URL}/api/user/create_session_id/`,
|
| 198 |
+
{
|
| 199 |
+
session_properties: JSON.stringify({ deviceId: this.deviceId }),
|
| 200 |
+
session_type: 1
|
| 201 |
+
}
|
| 202 |
+
);
|
| 203 |
+
return tokenResponse.data.session_id;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
private async captchaRequired(): Promise<boolean> {
|
| 207 |
+
const resp = await this.client.post(`${SunoApi.BASE_URL}/api/c/check`, {
|
| 208 |
+
ctype: 'generation'
|
| 209 |
+
});
|
| 210 |
+
logger.info(resp.data);
|
| 211 |
+
return resp.data.required;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
/**
|
| 215 |
+
* Clicks on a locator or XY vector. This method is made because of the difference between ghost-cursor-playwright and Playwright methods
|
| 216 |
+
*/
|
| 217 |
+
private async click(target: Locator|Page, position?: { x: number, y: number }): Promise<void> {
|
| 218 |
+
if (this.ghostCursorEnabled) {
|
| 219 |
+
let pos: any = isPage(target) ? { x: 0, y: 0 } : await target.boundingBox();
|
| 220 |
+
if (position)
|
| 221 |
+
pos = {
|
| 222 |
+
...pos,
|
| 223 |
+
x: pos.x + position.x,
|
| 224 |
+
y: pos.y + position.y,
|
| 225 |
+
width: null,
|
| 226 |
+
height: null,
|
| 227 |
+
};
|
| 228 |
+
return this.cursor?.actions.click({
|
| 229 |
+
target: pos
|
| 230 |
+
});
|
| 231 |
+
} else {
|
| 232 |
+
if (isPage(target))
|
| 233 |
+
return target.mouse.click(position?.x ?? 0, position?.y ?? 0);
|
| 234 |
+
else
|
| 235 |
+
return target.click({ force: true, position });
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/**
|
| 240 |
+
* Get the BrowserType from the `BROWSER` environment variable.
|
| 241 |
+
* @returns {BrowserType} chromium, firefox or webkit. Default is chromium
|
| 242 |
+
*/
|
| 243 |
+
private getBrowserType() {
|
| 244 |
+
const browser = process.env.BROWSER?.toLowerCase();
|
| 245 |
+
switch (browser) {
|
| 246 |
+
case 'firefox':
|
| 247 |
+
return firefox;
|
| 248 |
+
/*case 'webkit': ** doesn't work with rebrowser-patches
|
| 249 |
+
case 'safari':
|
| 250 |
+
return webkit;*/
|
| 251 |
+
default:
|
| 252 |
+
return chromium;
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
/**
|
| 257 |
+
* Launches a browser with the necessary cookies
|
| 258 |
+
* @returns {BrowserContext}
|
| 259 |
+
*/
|
| 260 |
+
private async launchBrowser(): Promise<BrowserContext> {
|
| 261 |
+
const args = [
|
| 262 |
+
'--disable-blink-features=AutomationControlled',
|
| 263 |
+
'--disable-web-security',
|
| 264 |
+
'--no-sandbox',
|
| 265 |
+
'--disable-dev-shm-usage',
|
| 266 |
+
'--disable-features=site-per-process',
|
| 267 |
+
'--disable-features=IsolateOrigins',
|
| 268 |
+
'--disable-extensions',
|
| 269 |
+
'--disable-infobars'
|
| 270 |
+
];
|
| 271 |
+
// Check for GPU acceleration, as it is recommended to turn it off for Docker
|
| 272 |
+
if (yn(process.env.BROWSER_DISABLE_GPU, { default: false }))
|
| 273 |
+
args.push('--enable-unsafe-swiftshader',
|
| 274 |
+
'--disable-gpu',
|
| 275 |
+
'--disable-setuid-sandbox');
|
| 276 |
+
const browser = await this.getBrowserType().launch({
|
| 277 |
+
args,
|
| 278 |
+
headless: yn(process.env.BROWSER_HEADLESS, { default: true })
|
| 279 |
+
});
|
| 280 |
+
const context = await browser.newContext({ userAgent: this.userAgent, locale: process.env.BROWSER_LOCALE, viewport: null });
|
| 281 |
+
const cookies = [];
|
| 282 |
+
const lax: 'Lax' | 'Strict' | 'None' = 'Lax';
|
| 283 |
+
cookies.push({
|
| 284 |
+
name: '__session',
|
| 285 |
+
value: this.currentToken+'',
|
| 286 |
+
domain: '.suno.com',
|
| 287 |
+
path: '/',
|
| 288 |
+
sameSite: lax
|
| 289 |
+
});
|
| 290 |
+
for (const key in this.cookies) {
|
| 291 |
+
cookies.push({
|
| 292 |
+
name: key,
|
| 293 |
+
value: this.cookies[key]+'',
|
| 294 |
+
domain: '.suno.com',
|
| 295 |
+
path: '/',
|
| 296 |
+
sameSite: lax
|
| 297 |
+
})
|
| 298 |
+
}
|
| 299 |
+
await context.addCookies(cookies);
|
| 300 |
+
return context;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/**
|
| 304 |
+
* Checks for CAPTCHA verification and solves the CAPTCHA if needed
|
| 305 |
+
* @returns {string|null} hCaptcha token. If no verification is required, returns null
|
| 306 |
+
*/
|
| 307 |
+
public async getCaptcha(): Promise<string|null> {
|
| 308 |
+
if (!await this.captchaRequired())
|
| 309 |
+
return null;
|
| 310 |
+
|
| 311 |
+
logger.info('CAPTCHA required. Launching browser...')
|
| 312 |
+
const browser = await this.launchBrowser();
|
| 313 |
+
const page = await browser.newPage();
|
| 314 |
+
await page.goto('https://suno.com/create', { referer: 'https://www.google.com/', waitUntil: 'domcontentloaded', timeout: 0 });
|
| 315 |
+
|
| 316 |
+
logger.info('Waiting for Suno interface to load');
|
| 317 |
+
// await page.locator('.react-aria-GridList').waitFor({ timeout: 60000 });
|
| 318 |
+
await page.waitForResponse('**/api/project/**\\?**', { timeout: 60000 }); // wait for song list API call
|
| 319 |
+
|
| 320 |
+
if (this.ghostCursorEnabled)
|
| 321 |
+
this.cursor = await createCursor(page);
|
| 322 |
+
|
| 323 |
+
logger.info('Triggering the CAPTCHA');
|
| 324 |
+
try {
|
| 325 |
+
await page.getByLabel('Close').click({ timeout: 2000 }); // close all popups
|
| 326 |
+
// await this.click(page, { x: 318, y: 13 });
|
| 327 |
+
} catch(e) {}
|
| 328 |
+
|
| 329 |
+
const textarea = page.locator('.custom-textarea');
|
| 330 |
+
await this.click(textarea);
|
| 331 |
+
await textarea.pressSequentially('Lorem ipsum', { delay: 80 });
|
| 332 |
+
|
| 333 |
+
const button = page.locator('button[aria-label="Create"]').locator('div.flex');
|
| 334 |
+
this.click(button);
|
| 335 |
+
|
| 336 |
+
const controller = new AbortController();
|
| 337 |
+
new Promise<void>(async (resolve, reject) => {
|
| 338 |
+
const frame = page.frameLocator('iframe[title*="hCaptcha"]');
|
| 339 |
+
const challenge = frame.locator('.challenge-container');
|
| 340 |
+
try {
|
| 341 |
+
let wait = true;
|
| 342 |
+
while (true) {
|
| 343 |
+
if (wait)
|
| 344 |
+
await waitForRequests(page, controller.signal);
|
| 345 |
+
const drag = (await challenge.locator('.prompt-text').first().innerText()).toLowerCase().includes('drag');
|
| 346 |
+
let captcha: any;
|
| 347 |
+
for (let j = 0; j < 3; j++) { // try several times because sometimes 2Captcha could return an error
|
| 348 |
+
try {
|
| 349 |
+
logger.info('Sending the CAPTCHA to 2Captcha');
|
| 350 |
+
const payload: paramsCoordinates = {
|
| 351 |
+
body: (await challenge.screenshot({ timeout: 5000 })).toString('base64'),
|
| 352 |
+
lang: process.env.BROWSER_LOCALE
|
| 353 |
+
};
|
| 354 |
+
if (drag) {
|
| 355 |
+
// Say to the worker that he needs to click
|
| 356 |
+
payload.textinstructions = 'CLICK on the shapes at their edge or center as shown above—please be precise!';
|
| 357 |
+
payload.imginstructions = (await fs.readFile(path.join(process.cwd(), 'public', 'drag-instructions.jpg'))).toString('base64');
|
| 358 |
+
}
|
| 359 |
+
captcha = await this.solver.coordinates(payload);
|
| 360 |
+
break;
|
| 361 |
+
} catch(err: any) {
|
| 362 |
+
logger.info(err.message);
|
| 363 |
+
if (j != 2)
|
| 364 |
+
logger.info('Retrying...');
|
| 365 |
+
else
|
| 366 |
+
throw err;
|
| 367 |
+
}
|
| 368 |
+
}
|
| 369 |
+
if (drag) {
|
| 370 |
+
const challengeBox = await challenge.boundingBox();
|
| 371 |
+
if (challengeBox == null)
|
| 372 |
+
throw new Error('.challenge-container boundingBox is null!');
|
| 373 |
+
if (captcha.data.length % 2) {
|
| 374 |
+
logger.info('Solution does not have even amount of points required for dragging. Requesting new solution...');
|
| 375 |
+
this.solver.badReport(captcha.id);
|
| 376 |
+
wait = false;
|
| 377 |
+
continue;
|
| 378 |
+
}
|
| 379 |
+
for (let i = 0; i < captcha.data.length; i += 2) {
|
| 380 |
+
const data1 = captcha.data[i];
|
| 381 |
+
const data2 = captcha.data[i+1];
|
| 382 |
+
logger.info(JSON.stringify(data1) + JSON.stringify(data2));
|
| 383 |
+
await page.mouse.move(challengeBox.x + +data1.x, challengeBox.y + +data1.y);
|
| 384 |
+
await page.mouse.down();
|
| 385 |
+
await sleep(1.1); // wait for the piece to be 'unlocked'
|
| 386 |
+
await page.mouse.move(challengeBox.x + +data2.x, challengeBox.y + +data2.y, { steps: 30 });
|
| 387 |
+
await page.mouse.up();
|
| 388 |
+
}
|
| 389 |
+
wait = true;
|
| 390 |
+
} else {
|
| 391 |
+
for (const data of captcha.data) {
|
| 392 |
+
logger.info(data);
|
| 393 |
+
await this.click(challenge, { x: +data.x, y: +data.y });
|
| 394 |
+
};
|
| 395 |
+
}
|
| 396 |
+
this.click(frame.locator('.button-submit')).catch(e => {
|
| 397 |
+
if (e.message.includes('viewport')) // when hCaptcha window has been closed due to inactivity,
|
| 398 |
+
this.click(button); // click the Create button again to trigger the CAPTCHA
|
| 399 |
+
else
|
| 400 |
+
throw e;
|
| 401 |
+
});
|
| 402 |
+
}
|
| 403 |
+
} catch(e: any) {
|
| 404 |
+
if (e.message.includes('been closed') // catch error when closing the browser
|
| 405 |
+
|| e.message == 'AbortError') // catch error when waitForRequests is aborted
|
| 406 |
+
resolve();
|
| 407 |
+
else
|
| 408 |
+
reject(e);
|
| 409 |
+
}
|
| 410 |
+
}).catch(e => {
|
| 411 |
+
browser.browser()?.close();
|
| 412 |
+
throw e;
|
| 413 |
+
});
|
| 414 |
+
return (new Promise((resolve, reject) => {
|
| 415 |
+
page.route('**/api/generate/v2/**', async (route: any) => {
|
| 416 |
+
try {
|
| 417 |
+
logger.info('hCaptcha token received. Closing browser');
|
| 418 |
+
route.abort();
|
| 419 |
+
browser.browser()?.close();
|
| 420 |
+
controller.abort();
|
| 421 |
+
const request = route.request();
|
| 422 |
+
this.currentToken = request.headers().authorization.split('Bearer ').pop();
|
| 423 |
+
resolve(request.postDataJSON().token);
|
| 424 |
+
} catch(err) {
|
| 425 |
+
reject(err);
|
| 426 |
+
}
|
| 427 |
+
});
|
| 428 |
+
}));
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/**
|
| 432 |
+
* Imitates Cloudflare Turnstile loading error. Unused right now, left for future
|
| 433 |
+
*/
|
| 434 |
+
private async getTurnstile() {
|
| 435 |
+
return this.client.post(
|
| 436 |
+
`https://clerk.suno.com/v1/client?__clerk_api_version=2021-02-05&_clerk_js_version=${SunoApi.CLERK_VERSION}&_method=PATCH`,
|
| 437 |
+
{ captcha_error: '300030,300030,300030' },
|
| 438 |
+
{ headers: { 'content-type': 'application/x-www-form-urlencoded' } });
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
/**
|
| 442 |
+
* Generate a song based on the prompt.
|
| 443 |
+
* @param prompt The text prompt to generate audio from.
|
| 444 |
+
* @param make_instrumental Indicates if the generated audio should be instrumental.
|
| 445 |
+
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
|
| 446 |
+
* @returns
|
| 447 |
+
*/
|
| 448 |
+
public async generate(
|
| 449 |
+
prompt: string,
|
| 450 |
+
make_instrumental: boolean = false,
|
| 451 |
+
model?: string,
|
| 452 |
+
wait_audio: boolean = false
|
| 453 |
+
): Promise<AudioInfo[]> {
|
| 454 |
+
await this.keepAlive(false);
|
| 455 |
+
const startTime = Date.now();
|
| 456 |
+
const audios = await this.generateSongs(
|
| 457 |
+
prompt,
|
| 458 |
+
false,
|
| 459 |
+
undefined,
|
| 460 |
+
undefined,
|
| 461 |
+
make_instrumental,
|
| 462 |
+
model,
|
| 463 |
+
wait_audio
|
| 464 |
+
);
|
| 465 |
+
const costTime = Date.now() - startTime;
|
| 466 |
+
logger.info('Generate Response:\n' + JSON.stringify(audios, null, 2));
|
| 467 |
+
logger.info('Cost time: ' + costTime);
|
| 468 |
+
return audios;
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
/**
|
| 472 |
+
* Calls the concatenate endpoint for a clip to generate the whole song.
|
| 473 |
+
* @param clip_id The ID of the audio clip to concatenate.
|
| 474 |
+
* @returns A promise that resolves to an AudioInfo object representing the concatenated audio.
|
| 475 |
+
* @throws Error if the response status is not 200.
|
| 476 |
+
*/
|
| 477 |
+
public async concatenate(clip_id: string): Promise<AudioInfo> {
|
| 478 |
+
await this.keepAlive(false);
|
| 479 |
+
const payload: any = { clip_id: clip_id };
|
| 480 |
+
|
| 481 |
+
const response = await this.client.post(
|
| 482 |
+
`${SunoApi.BASE_URL}/api/generate/concat/v2/`,
|
| 483 |
+
payload,
|
| 484 |
+
{
|
| 485 |
+
timeout: 10000 // 10 seconds timeout
|
| 486 |
+
}
|
| 487 |
+
);
|
| 488 |
+
if (response.status !== 200) {
|
| 489 |
+
throw new Error('Error response:' + response.statusText);
|
| 490 |
+
}
|
| 491 |
+
return response.data;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
/**
|
| 495 |
+
* Generates custom audio based on provided parameters.
|
| 496 |
+
*
|
| 497 |
+
* @param prompt The text prompt to generate audio from.
|
| 498 |
+
* @param tags Tags to categorize the generated audio.
|
| 499 |
+
* @param title The title for the generated audio.
|
| 500 |
+
* @param make_instrumental Indicates if the generated audio should be instrumental.
|
| 501 |
+
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
|
| 502 |
+
* @param negative_tags Negative tags that should not be included in the generated audio.
|
| 503 |
+
* @returns A promise that resolves to an array of AudioInfo objects representing the generated audios.
|
| 504 |
+
*/
|
| 505 |
+
public async custom_generate(
|
| 506 |
+
prompt: string,
|
| 507 |
+
tags: string,
|
| 508 |
+
title: string,
|
| 509 |
+
make_instrumental: boolean = false,
|
| 510 |
+
model?: string,
|
| 511 |
+
wait_audio: boolean = false,
|
| 512 |
+
negative_tags?: string
|
| 513 |
+
): Promise<AudioInfo[]> {
|
| 514 |
+
const startTime = Date.now();
|
| 515 |
+
const audios = await this.generateSongs(
|
| 516 |
+
prompt,
|
| 517 |
+
true,
|
| 518 |
+
tags,
|
| 519 |
+
title,
|
| 520 |
+
make_instrumental,
|
| 521 |
+
model,
|
| 522 |
+
wait_audio,
|
| 523 |
+
negative_tags
|
| 524 |
+
);
|
| 525 |
+
const costTime = Date.now() - startTime;
|
| 526 |
+
logger.info(
|
| 527 |
+
'Custom Generate Response:\n' + JSON.stringify(audios, null, 2)
|
| 528 |
+
);
|
| 529 |
+
logger.info('Cost time: ' + costTime);
|
| 530 |
+
return audios;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
/**
|
| 534 |
+
* Generates songs based on the provided parameters.
|
| 535 |
+
*
|
| 536 |
+
* @param prompt The text prompt to generate songs from.
|
| 537 |
+
* @param isCustom Indicates if the generation should consider custom parameters like tags and title.
|
| 538 |
+
* @param tags Optional tags to categorize the song, used only if isCustom is true.
|
| 539 |
+
* @param title Optional title for the song, used only if isCustom is true.
|
| 540 |
+
* @param make_instrumental Indicates if the generated song should be instrumental.
|
| 541 |
+
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
|
| 542 |
+
* @param negative_tags Negative tags that should not be included in the generated audio.
|
| 543 |
+
* @param task Optional indication of what to do. Enter 'extend' if extending an audio, otherwise specify null.
|
| 544 |
+
* @param continue_clip_id
|
| 545 |
+
* @returns A promise that resolves to an array of AudioInfo objects representing the generated songs.
|
| 546 |
+
*/
|
| 547 |
+
private async generateSongs(
|
| 548 |
+
prompt: string,
|
| 549 |
+
isCustom: boolean,
|
| 550 |
+
tags?: string,
|
| 551 |
+
title?: string,
|
| 552 |
+
make_instrumental?: boolean,
|
| 553 |
+
model?: string,
|
| 554 |
+
wait_audio: boolean = false,
|
| 555 |
+
negative_tags?: string,
|
| 556 |
+
task?: string,
|
| 557 |
+
continue_clip_id?: string,
|
| 558 |
+
continue_at?: number
|
| 559 |
+
): Promise<AudioInfo[]> {
|
| 560 |
+
await this.keepAlive();
|
| 561 |
+
const payload: any = {
|
| 562 |
+
make_instrumental: make_instrumental,
|
| 563 |
+
mv: model || DEFAULT_MODEL,
|
| 564 |
+
prompt: '',
|
| 565 |
+
generation_type: 'TEXT',
|
| 566 |
+
continue_at: continue_at,
|
| 567 |
+
continue_clip_id: continue_clip_id,
|
| 568 |
+
task: task,
|
| 569 |
+
token: await this.getCaptcha()
|
| 570 |
+
};
|
| 571 |
+
if (isCustom) {
|
| 572 |
+
payload.tags = tags;
|
| 573 |
+
payload.title = title;
|
| 574 |
+
payload.negative_tags = negative_tags;
|
| 575 |
+
payload.prompt = prompt;
|
| 576 |
+
} else {
|
| 577 |
+
payload.gpt_description_prompt = prompt;
|
| 578 |
+
}
|
| 579 |
+
logger.info(
|
| 580 |
+
'generateSongs payload:\n' +
|
| 581 |
+
JSON.stringify(
|
| 582 |
+
{
|
| 583 |
+
prompt: prompt,
|
| 584 |
+
isCustom: isCustom,
|
| 585 |
+
tags: tags,
|
| 586 |
+
title: title,
|
| 587 |
+
make_instrumental: make_instrumental,
|
| 588 |
+
wait_audio: wait_audio,
|
| 589 |
+
negative_tags: negative_tags,
|
| 590 |
+
payload: payload
|
| 591 |
+
},
|
| 592 |
+
null,
|
| 593 |
+
2
|
| 594 |
+
)
|
| 595 |
+
);
|
| 596 |
+
const response = await this.client.post(
|
| 597 |
+
`${SunoApi.BASE_URL}/api/generate/v2/`,
|
| 598 |
+
payload,
|
| 599 |
+
{
|
| 600 |
+
timeout: 10000 // 10 seconds timeout
|
| 601 |
+
}
|
| 602 |
+
);
|
| 603 |
+
if (response.status !== 200) {
|
| 604 |
+
throw new Error('Error response:' + response.statusText);
|
| 605 |
+
}
|
| 606 |
+
const songIds = response.data.clips.map((audio: any) => audio.id);
|
| 607 |
+
//Want to wait for music file generation
|
| 608 |
+
if (wait_audio) {
|
| 609 |
+
const startTime = Date.now();
|
| 610 |
+
let lastResponse: AudioInfo[] = [];
|
| 611 |
+
await sleep(5, 5);
|
| 612 |
+
while (Date.now() - startTime < 100000) {
|
| 613 |
+
const response = await this.get(songIds);
|
| 614 |
+
const allCompleted = response.every(
|
| 615 |
+
(audio) => audio.status === 'streaming' || audio.status === 'complete'
|
| 616 |
+
);
|
| 617 |
+
const allError = response.every((audio) => audio.status === 'error');
|
| 618 |
+
if (allCompleted || allError) {
|
| 619 |
+
return response;
|
| 620 |
+
}
|
| 621 |
+
lastResponse = response;
|
| 622 |
+
await sleep(3, 6);
|
| 623 |
+
await this.keepAlive(true);
|
| 624 |
+
}
|
| 625 |
+
return lastResponse;
|
| 626 |
+
} else {
|
| 627 |
+
return response.data.clips.map((audio: any) => ({
|
| 628 |
+
id: audio.id,
|
| 629 |
+
title: audio.title,
|
| 630 |
+
image_url: audio.image_url,
|
| 631 |
+
lyric: audio.metadata.prompt,
|
| 632 |
+
audio_url: audio.audio_url,
|
| 633 |
+
video_url: audio.video_url,
|
| 634 |
+
created_at: audio.created_at,
|
| 635 |
+
model_name: audio.model_name,
|
| 636 |
+
status: audio.status,
|
| 637 |
+
gpt_description_prompt: audio.metadata.gpt_description_prompt,
|
| 638 |
+
prompt: audio.metadata.prompt,
|
| 639 |
+
type: audio.metadata.type,
|
| 640 |
+
tags: audio.metadata.tags,
|
| 641 |
+
negative_tags: audio.metadata.negative_tags,
|
| 642 |
+
duration: audio.metadata.duration
|
| 643 |
+
}));
|
| 644 |
+
}
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
/**
|
| 648 |
+
* Generates lyrics based on a given prompt.
|
| 649 |
+
* @param prompt The prompt for generating lyrics.
|
| 650 |
+
* @returns The generated lyrics text.
|
| 651 |
+
*/
|
| 652 |
+
public async generateLyrics(prompt: string): Promise<string> {
|
| 653 |
+
await this.keepAlive(false);
|
| 654 |
+
// Initiate lyrics generation
|
| 655 |
+
const generateResponse = await this.client.post(
|
| 656 |
+
`${SunoApi.BASE_URL}/api/generate/lyrics/`,
|
| 657 |
+
{ prompt }
|
| 658 |
+
);
|
| 659 |
+
const generateId = generateResponse.data.id;
|
| 660 |
+
|
| 661 |
+
// Poll for lyrics completion
|
| 662 |
+
let lyricsResponse = await this.client.get(
|
| 663 |
+
`${SunoApi.BASE_URL}/api/generate/lyrics/${generateId}`
|
| 664 |
+
);
|
| 665 |
+
while (lyricsResponse?.data?.status !== 'complete') {
|
| 666 |
+
await sleep(2); // Wait for 2 seconds before polling again
|
| 667 |
+
lyricsResponse = await this.client.get(
|
| 668 |
+
`${SunoApi.BASE_URL}/api/generate/lyrics/${generateId}`
|
| 669 |
+
);
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
// Return the generated lyrics text
|
| 673 |
+
return lyricsResponse.data;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
/**
|
| 677 |
+
* Extends an existing audio clip by generating additional content based on the provided prompt.
|
| 678 |
+
*
|
| 679 |
+
* @param audioId The ID of the audio clip to extend.
|
| 680 |
+
* @param prompt The prompt for generating additional content.
|
| 681 |
+
* @param continueAt Extend a new clip from a song at mm:ss(e.g. 00:30). Default extends from the end of the song.
|
| 682 |
+
* @param tags Style of Music.
|
| 683 |
+
* @param title Title of the song.
|
| 684 |
+
* @returns A promise that resolves to an AudioInfo object representing the extended audio clip.
|
| 685 |
+
*/
|
| 686 |
+
public async extendAudio(
|
| 687 |
+
audioId: string,
|
| 688 |
+
prompt: string = '',
|
| 689 |
+
continueAt: number,
|
| 690 |
+
tags: string = '',
|
| 691 |
+
negative_tags: string = '',
|
| 692 |
+
title: string = '',
|
| 693 |
+
model?: string,
|
| 694 |
+
wait_audio?: boolean
|
| 695 |
+
): Promise<AudioInfo[]> {
|
| 696 |
+
return this.generateSongs(prompt, true, tags, title, false, model, wait_audio, negative_tags, 'extend', audioId, continueAt);
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
/**
|
| 700 |
+
* Generate stems for a song.
|
| 701 |
+
* @param song_id The ID of the song to generate stems for.
|
| 702 |
+
* @returns A promise that resolves to an AudioInfo object representing the generated stems.
|
| 703 |
+
*/
|
| 704 |
+
public async generateStems(song_id: string): Promise<AudioInfo[]> {
|
| 705 |
+
await this.keepAlive(false);
|
| 706 |
+
const response = await this.client.post(
|
| 707 |
+
`${SunoApi.BASE_URL}/api/edit/stems/${song_id}`, {}
|
| 708 |
+
);
|
| 709 |
+
|
| 710 |
+
console.log('generateStems response:\n', response?.data);
|
| 711 |
+
return response.data.clips.map((clip: any) => ({
|
| 712 |
+
id: clip.id,
|
| 713 |
+
status: clip.status,
|
| 714 |
+
created_at: clip.created_at,
|
| 715 |
+
title: clip.title,
|
| 716 |
+
stem_from_id: clip.metadata.stem_from_id,
|
| 717 |
+
duration: clip.metadata.duration
|
| 718 |
+
}));
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
/**
|
| 723 |
+
* Get the lyric alignment for a song.
|
| 724 |
+
* @param song_id The ID of the song to get the lyric alignment for.
|
| 725 |
+
* @returns A promise that resolves to an object containing the lyric alignment.
|
| 726 |
+
*/
|
| 727 |
+
public async getLyricAlignment(song_id: string): Promise<object> {
|
| 728 |
+
await this.keepAlive(false);
|
| 729 |
+
const response = await this.client.get(`${SunoApi.BASE_URL}/api/gen/${song_id}/aligned_lyrics/v2/`);
|
| 730 |
+
|
| 731 |
+
console.log(`getLyricAlignment ~ response:`, response.data);
|
| 732 |
+
return response.data?.aligned_words.map((transcribedWord: any) => ({
|
| 733 |
+
word: transcribedWord.word,
|
| 734 |
+
start_s: transcribedWord.start_s,
|
| 735 |
+
end_s: transcribedWord.end_s,
|
| 736 |
+
success: transcribedWord.success,
|
| 737 |
+
p_align: transcribedWord.p_align
|
| 738 |
+
}));
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
/**
|
| 742 |
+
* Processes the lyrics (prompt) from the audio metadata into a more readable format.
|
| 743 |
+
* @param prompt The original lyrics text.
|
| 744 |
+
* @returns The processed lyrics text.
|
| 745 |
+
*/
|
| 746 |
+
private parseLyrics(prompt: string): string {
|
| 747 |
+
// Assuming the original lyrics are separated by a specific delimiter (e.g., newline), we can convert it into a more readable format.
|
| 748 |
+
// The implementation here can be adjusted according to the actual lyrics format.
|
| 749 |
+
// 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.).
|
| 750 |
+
// The following implementation assumes that the lyrics are already separated by newlines.
|
| 751 |
+
|
| 752 |
+
// Split the lyrics using newline and ensure to remove empty lines.
|
| 753 |
+
const lines = prompt.split('\n').filter((line) => line.trim() !== '');
|
| 754 |
+
|
| 755 |
+
// Reassemble the processed lyrics lines into a single string, separated by newlines between each line.
|
| 756 |
+
// Additional formatting logic can be added here, such as adding specific markers or handling special lines.
|
| 757 |
+
return lines.join('\n');
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
/**
|
| 761 |
+
* Retrieves audio information for the given song IDs.
|
| 762 |
+
* @param songIds An optional array of song IDs to retrieve information for.
|
| 763 |
+
* @param page An optional page number to retrieve audio information from.
|
| 764 |
+
* @returns A promise that resolves to an array of AudioInfo objects.
|
| 765 |
+
*/
|
| 766 |
+
public async get(
|
| 767 |
+
songIds?: string[],
|
| 768 |
+
page?: string | null
|
| 769 |
+
): Promise<AudioInfo[]> {
|
| 770 |
+
await this.keepAlive(false);
|
| 771 |
+
let url = new URL(`${SunoApi.BASE_URL}/api/feed/v2`);
|
| 772 |
+
if (songIds) {
|
| 773 |
+
url.searchParams.append('ids', songIds.join(','));
|
| 774 |
+
}
|
| 775 |
+
if (page) {
|
| 776 |
+
url.searchParams.append('page', page);
|
| 777 |
+
}
|
| 778 |
+
logger.info('Get audio status: ' + url.href);
|
| 779 |
+
const response = await this.client.get(url.href, {
|
| 780 |
+
// 10 seconds timeout
|
| 781 |
+
timeout: 10000
|
| 782 |
+
});
|
| 783 |
+
|
| 784 |
+
const audios = response.data.clips;
|
| 785 |
+
|
| 786 |
+
return audios.map((audio: any) => ({
|
| 787 |
+
id: audio.id,
|
| 788 |
+
title: audio.title,
|
| 789 |
+
image_url: audio.image_url,
|
| 790 |
+
lyric: audio.metadata.prompt
|
| 791 |
+
? this.parseLyrics(audio.metadata.prompt)
|
| 792 |
+
: '',
|
| 793 |
+
audio_url: audio.audio_url,
|
| 794 |
+
video_url: audio.video_url,
|
| 795 |
+
created_at: audio.created_at,
|
| 796 |
+
model_name: audio.model_name,
|
| 797 |
+
status: audio.status,
|
| 798 |
+
gpt_description_prompt: audio.metadata.gpt_description_prompt,
|
| 799 |
+
prompt: audio.metadata.prompt,
|
| 800 |
+
type: audio.metadata.type,
|
| 801 |
+
tags: audio.metadata.tags,
|
| 802 |
+
duration: audio.metadata.duration,
|
| 803 |
+
error_message: audio.metadata.error_message
|
| 804 |
+
}));
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
/**
|
| 808 |
+
* Retrieves information for a specific audio clip.
|
| 809 |
+
* @param clipId The ID of the audio clip to retrieve information for.
|
| 810 |
+
* @returns A promise that resolves to an object containing the audio clip information.
|
| 811 |
+
*/
|
| 812 |
+
public async getClip(clipId: string): Promise<object> {
|
| 813 |
+
await this.keepAlive(false);
|
| 814 |
+
const response = await this.client.get(
|
| 815 |
+
`${SunoApi.BASE_URL}/api/clip/${clipId}`
|
| 816 |
+
);
|
| 817 |
+
return response.data;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
public async get_credits(): Promise<object> {
|
| 821 |
+
await this.keepAlive(false);
|
| 822 |
+
const response = await this.client.get(
|
| 823 |
+
`${SunoApi.BASE_URL}/api/billing/info/`
|
| 824 |
+
);
|
| 825 |
+
return {
|
| 826 |
+
credits_left: response.data.total_credits_left,
|
| 827 |
+
period: response.data.period,
|
| 828 |
+
monthly_limit: response.data.monthly_limit,
|
| 829 |
+
monthly_usage: response.data.monthly_usage
|
| 830 |
+
};
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
public async getPersonaPaginated(personaId: string, page: number = 1): Promise<PersonaResponse> {
|
| 834 |
+
await this.keepAlive(false);
|
| 835 |
+
|
| 836 |
+
const url = `${SunoApi.BASE_URL}/api/persona/get-persona-paginated/${personaId}/?page=${page}`;
|
| 837 |
+
|
| 838 |
+
logger.info(`Fetching persona data: ${url}`);
|
| 839 |
+
|
| 840 |
+
const response = await this.client.get(url, {
|
| 841 |
+
timeout: 10000 // 10 seconds timeout
|
| 842 |
+
});
|
| 843 |
+
|
| 844 |
+
if (response.status !== 200) {
|
| 845 |
+
throw new Error('Error response: ' + response.statusText);
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
return response.data;
|
| 849 |
+
}
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
export const sunoApi = async (cookie?: string) => {
|
| 853 |
+
const resolvedCookie = cookie && cookie.includes('__client') ? cookie : process.env.SUNO_COOKIE; // Check for bad `Cookie` header (It's too expensive to actually parse the cookies *here*)
|
| 854 |
+
if (!resolvedCookie) {
|
| 855 |
+
logger.info('No cookie provided! Aborting...\nPlease provide a cookie either in the .env file or in the Cookie header of your request.')
|
| 856 |
+
throw new Error('Please provide a cookie either in the .env file or in the Cookie header of your request.');
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
// Check if the instance for this cookie already exists in the cache
|
| 860 |
+
const cachedInstance = cache.get(resolvedCookie);
|
| 861 |
+
if (cachedInstance)
|
| 862 |
+
return cachedInstance;
|
| 863 |
+
|
| 864 |
+
// If not, create a new instance and initialize it
|
| 865 |
+
const instance = await new SunoApi(resolvedCookie).init();
|
| 866 |
+
// Cache the initialized instance
|
| 867 |
+
cache.set(resolvedCookie, instance);
|
| 868 |
+
|
| 869 |
+
return instance;
|
| 870 |
+
};
|
src/lib/utils.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pino from "pino";
|
| 2 |
+
import { Page } from "rebrowser-playwright-core";
|
| 3 |
+
|
| 4 |
+
const logger = pino();
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Pause for a specified number of seconds.
|
| 8 |
+
* @param x Minimum number of seconds.
|
| 9 |
+
* @param y Maximum number of seconds (optional).
|
| 10 |
+
*/
|
| 11 |
+
export const sleep = (x: number, y?: number): Promise<void> => {
|
| 12 |
+
let timeout = x * 1000;
|
| 13 |
+
if (y !== undefined && y !== x) {
|
| 14 |
+
const min = Math.min(x, y);
|
| 15 |
+
const max = Math.max(x, y);
|
| 16 |
+
timeout = Math.floor(Math.random() * (max - min + 1) + min) * 1000;
|
| 17 |
+
}
|
| 18 |
+
// console.log(`Sleeping for ${timeout / 1000} seconds`);
|
| 19 |
+
logger.info(`Sleeping for ${timeout / 1000} seconds`);
|
| 20 |
+
|
| 21 |
+
return new Promise(resolve => setTimeout(resolve, timeout));
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* @param target A Locator or a page
|
| 26 |
+
* @returns {boolean}
|
| 27 |
+
*/
|
| 28 |
+
export const isPage = (target: any): target is Page => {
|
| 29 |
+
return target.constructor.name === 'Page';
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Waits for an hCaptcha image requests and then waits for all of them to end
|
| 34 |
+
* @param page
|
| 35 |
+
* @param signal `const controller = new AbortController(); controller.status`
|
| 36 |
+
* @returns {Promise<void>}
|
| 37 |
+
*/
|
| 38 |
+
export const waitForRequests = (page: Page, signal: AbortSignal): Promise<void> => {
|
| 39 |
+
return new Promise((resolve, reject) => {
|
| 40 |
+
const urlPattern = /^https:\/\/img[a-zA-Z0-9]*\.hcaptcha\.com\/.*$/;
|
| 41 |
+
let timeoutHandle: NodeJS.Timeout | null = null;
|
| 42 |
+
let activeRequestCount = 0;
|
| 43 |
+
let requestOccurred = false;
|
| 44 |
+
|
| 45 |
+
const cleanupListeners = () => {
|
| 46 |
+
page.off('request', onRequest);
|
| 47 |
+
page.off('requestfinished', onRequestFinished);
|
| 48 |
+
page.off('requestfailed', onRequestFinished);
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
const resetTimeout = () => {
|
| 52 |
+
if (timeoutHandle)
|
| 53 |
+
clearTimeout(timeoutHandle);
|
| 54 |
+
if (activeRequestCount === 0) {
|
| 55 |
+
timeoutHandle = setTimeout(() => {
|
| 56 |
+
cleanupListeners();
|
| 57 |
+
resolve();
|
| 58 |
+
}, 1000); // 1 second of no requests
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
const onRequest = (request: { url: () => string }) => {
|
| 63 |
+
if (urlPattern.test(request.url())) {
|
| 64 |
+
requestOccurred = true;
|
| 65 |
+
activeRequestCount++;
|
| 66 |
+
if (timeoutHandle)
|
| 67 |
+
clearTimeout(timeoutHandle);
|
| 68 |
+
}
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
const onRequestFinished = (request: { url: () => string }) => {
|
| 72 |
+
if (urlPattern.test(request.url())) {
|
| 73 |
+
activeRequestCount--;
|
| 74 |
+
resetTimeout();
|
| 75 |
+
}
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
// Wait for an hCaptcha request for up to 1 minute
|
| 79 |
+
const initialTimeout = setTimeout(() => {
|
| 80 |
+
if (!requestOccurred) {
|
| 81 |
+
page.off('request', onRequest);
|
| 82 |
+
cleanupListeners();
|
| 83 |
+
reject(new Error('No hCaptcha request occurred within 1 minute.'));
|
| 84 |
+
} else {
|
| 85 |
+
// Start waiting for no hCaptcha requests
|
| 86 |
+
resetTimeout();
|
| 87 |
+
}
|
| 88 |
+
}, 60000); // 1 minute timeout
|
| 89 |
+
|
| 90 |
+
page.on('request', onRequest);
|
| 91 |
+
page.on('requestfinished', onRequestFinished);
|
| 92 |
+
page.on('requestfailed', onRequestFinished);
|
| 93 |
+
|
| 94 |
+
// Cleanup the initial timeout if an hCaptcha request occurs
|
| 95 |
+
page.on('request', (request: { url: () => string }) => {
|
| 96 |
+
if (urlPattern.test(request.url())) {
|
| 97 |
+
clearTimeout(initialTimeout);
|
| 98 |
+
}
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
const onAbort = () => {
|
| 102 |
+
cleanupListeners();
|
| 103 |
+
clearTimeout(initialTimeout);
|
| 104 |
+
if (timeoutHandle)
|
| 105 |
+
clearTimeout(timeoutHandle);
|
| 106 |
+
signal.removeEventListener('abort', onAbort);
|
| 107 |
+
reject(new Error('AbortError'));
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
signal.addEventListener('abort', onAbort, { once: true });
|
| 111 |
+
});
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
export const corsHeaders = {
|
| 115 |
+
'Access-Control-Allow-Origin': '*',
|
| 116 |
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
| 117 |
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
| 118 |
+
}
|
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;
|
tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
| 4 |
+
"allowJs": true,
|
| 5 |
+
"skipLibCheck": true,
|
| 6 |
+
"strict": true,
|
| 7 |
+
"noEmit": true,
|
| 8 |
+
"allowSyntheticDefaultImports": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"target": "ESNext",
|
| 11 |
+
"module": "ESNext",
|
| 12 |
+
"moduleResolution": "bundler",
|
| 13 |
+
"resolveJsonModule": true,
|
| 14 |
+
"isolatedModules": true,
|
| 15 |
+
"jsx": "preserve",
|
| 16 |
+
"incremental": true,
|
| 17 |
+
"plugins": [
|
| 18 |
+
{
|
| 19 |
+
"name": "next"
|
| 20 |
+
}
|
| 21 |
+
],
|
| 22 |
+
"paths": {
|
| 23 |
+
"@/*": ["./src/*"],
|
| 24 |
+
"playwright-core": ["./node_modules/rebrowser-playwright-core"]
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
| 28 |
+
"exclude": ["node_modules"]
|
| 29 |
+
}
|