Implement hCaptcha solving via 2Captcha & other stuff

- implement hCaptcha solving via paid service 2Captcha and browser automation library Playwright with rebrowser-patches

- implement sunoApi instances caching so sessions won't be constantly updated

- add support for entering cookies not only in SUNO_COOKIE, but also the Cookie HTTP header

- update docs and add Russian docs
This commit is contained in:
gohoski 2025-01-06 05:25:08 +03:00
parent 3bffec1ea1
commit 7da7ac6ae2
24 changed files with 10837 additions and 138 deletions

View File

@ -1,2 +1,7 @@
SUNO_COOKIE=<your-suno-cookie>
# For more information, please see the README.md
SUNO_COOKIE=
TWOCAPTCHA_KEY= # Obtain from 2captcha.com
BROWSER=chromium # chromium or firefox
BROWSER_GHOST_CURSOR=false
BROWSER_LOCALE=en
BROWSER_HEADLESS=true

View File

@ -12,10 +12,14 @@ WORKDIR /app
COPY package*.json ./
ARG SUNO_COOKIE
RUN if [ -z "$SUNO_COOKIE" ]; then echo "SUNO_COOKIE is not set" && exit 1; fi
ARG BROWSER
RUN if [ -z "$SUNO_COOKIE" ]; then echo "Warning: SUNO_COOKIE is not set"; fi
ENV SUNO_COOKIE=${SUNO_COOKIE}
RUN if [ -z "$BROWSER" ]; then echo "Warning: BROWSER is not set; will use chromium by default"; fi
ENV BROWSER=${BROWSER:-chromium}
RUN npm install --only=production
RUN npx playwright install $BROWSER
COPY --from=builder /src/.next ./.next
EXPOSE 3000
CMD ["npm", "run", "start"]

View File

@ -8,9 +8,10 @@
<p align="center">
<a target="_blank" href="./README.md">English</a>
| <a target="_blank" href="./README_CN.md">简体中文</a>
| <a target="_blank" href="./README_RU.md">русский</a>
| <a target="_blank" href="https://suno.gcui.ai">Demo</a>
| <a target="_blank" href="https://suno.gcui.ai/docs">Docs</a>
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api">Deploy with Vercel</a>
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api">Deploy with Vercel</a>
</p>
<p align="center">
<a href="https://www.producthunt.com/products/gcui-art-suno-api-open-source-sunoai-api/reviews?utm_source=badge-product_review&utm_medium=badge&utm_souce=badge-gcui&#0045;art&#0045;suno&#0045;api&#0045;open&#0045;source&#0045;sunoai&#0045;api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/product_review.svg?product_id=577408&theme=light" alt="gcui&#0045;art&#0047;suno&#0045;api&#0058;Open&#0045;source&#0032;SunoAI&#0032;API - Use&#0032;API&#0032;to&#0032;call&#0032;the&#0032;music&#0032;generation&#0032;AI&#0032;of&#0032;suno&#0046;ai&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
@ -22,10 +23,12 @@
## Introduction
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.
Suno is an amazing AI music service. Although the official API is not yet available, we couldn't wait to integrate its capabilities somewhere.
We discovered that some users have similar needs, so we decided to open-source this project, hoping you'll like it.
This implementation uses the paid [2Captcha](https://2captcha.com/about) service (a.k.a. ruCaptcha) to solve the hCaptcha challenges automatically and does not use any already made closed-source paid Suno API implementations.
## Demo
We have deployed an example bound to a free Suno account, so it has daily usage limits, but you can see how it runs:
@ -33,35 +36,43 @@ We have deployed an example bound to a free Suno account, so it has daily usage
## Features
- Perfectly implements the creation API from app.suno.ai
- Perfectly implements the creation API from suno.ai.
- Automatically keep the account active.
- Solve CAPTCHAs automatically using [2Captcha](https://2captcha.com) and [Playwright](https://playwright.dev) with [rebrowser-patches](https://github.com/rebrowser/rebrowser-patches).
- Compatible with the format of OpenAIs `/v1/chat/completions` API.
- Supports Custom Mode
- One-click deployment to Vercel
- Supports Custom Mode.
- One-click deployment to [Vercel](#deploy-to-vercel) & [Docker](#docker).
- 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.
- Permissive open-source license, allowing you to freely integrate and modify.
## Getting Started
### 1. Obtain the cookie of your app.suno.ai account
### 1. Obtain the cookie of your Suno account
1. Head over to [app.suno.ai](https://app.suno.ai) using your browser.
1. Head over to [suno.com/create](https://suno.com/create) using your browser.
2. Open up the browser console: hit `F12` or access the `Developer Tools`.
3. Navigate to the `Network tab`.
3. Navigate to the `Network` tab.
4. Give the page a quick refresh.
5. Identify the request that includes the keyword `client?_clerk_js_version`.
5. Identify the latest request that includes the keyword `?__clerk_api_version`.
6. Click on it and switch over to the `Header` tab.
7. Locate the `Cookie` section, hover your mouse over it, and copy the value of the Cookie.
![get cookie](https://github.com/gcui-art/suno-api/blob/main/public/get-cookie-demo.gif)
### 2. Clone and deploy this project
### 2. Register on 2Captcha and top up your balance
[2Captcha](https://2captcha.com/about) is a paid CAPTCHA solving service that uses real workers to solve the CAPTCHA and has high accuracy. It is needed because of Suno constantly requesting hCaptcha solving that currently isn't possible for free by any means.
[Create](https://2captcha.com/auth/register?userType=customer) a new 2Captcha account, [top up](https://2captcha.com/pay) your balance and [get your API key](https://2captcha.com/enterpage#recognition).
If you are located in Russia or Belarus, use the [ruCaptcha](https://rucaptcha.com) interface instead of 2Captcha. It's the same service, but it supports payments from those countries.
### 3. Clone and deploy this project
You can choose your preferred deployment method:
#### Deploy to Vercel
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api)
#### Run locally
@ -69,25 +80,37 @@ You can choose your preferred deployment method:
git clone https://github.com/gcui-art/suno-api.git
cd suno-api
npm install
npx playwright install chromium
```
Alternatively, you can use [Docker Compose](https://docs.docker.com/compose/)
#### Docker
Alternatively, you can use [Docker Compose](https://docs.docker.com/compose/). However, follow the step below before running.
```bash
docker compose build && docker compose up
```
### 3. Configure suno-api
### 4. Configure suno-api
- 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.
- If deployed to Vercel, please add the environment variables in the Vercel dashboard.
- If youre running this locally, be sure to add the following to your `.env` file:
#### Environment variables
- `SUNO_COOKIE` — the `Cookie` header you obtained in the first step.
- `TWOCAPTCHA_KEY` — your 2Captcha API key from the second step.
- `BROWSER` — the name of the browser that is going to be used to solve the CAPTCHA. Only `chromium` and `firefox` supported.
- `BROWSER_GHOST_CURSOR` — use ghost-cursor-playwright to simulate smooth mouse movements. Please note that it doesn't seem to make any difference in the rate of CAPTCHAs, so you can set it to `false`. Retained for future testing.
- `BROWSER_LOCALE` — the language of the browser. Using either `en` or `ru` is recommended, since those have the most workers on 2Captcha. [List of supported languages](https://2captcha.com/2captcha-api#language)
- `BROWSER_HEADLESS` — run the browser without the window. You probably want to set this to `true`.
```bash
SUNO_COOKIE=<your-cookie>
SUNO_COOKIE=<…>
TWOCAPTCHA_KEY=<…>
BROWSER=chromium
BROWSER_GHOST_CURSOR=false
BROWSER_LOCALE=en
BROWSER_HEADLESS=true
```
### 4. Run suno api
### 5. Run suno-api
- If youve deployed to Vercel:
- Please click on Deploy in the Vercel dashboard and wait for the deployment to be successful.
@ -108,7 +131,7 @@ SUNO_COOKIE=<your-cookie>
it means the program is running normally.
### 5. Use Suno API
### 6. Use Suno API
You can check out the detailed API documentation at :
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
@ -132,10 +155,12 @@ Suno API currently mainly implements the following APIs:
- `/api/concat`: Generate the whole song from extensions
```
You can also specify the cookies in the `Cookie` header of your request, overriding the default cookies in the `SUNO_COOKIE` environment variable. This comes in handy when, for example, you want to use multiple free accounts at the same time.
For more detailed documentation, please check out the demo site:
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
## API Integration Code Example
## API Integration Code Examples
### Python
@ -143,7 +168,7 @@ For more detailed documentation, please check out the demo site:
import time
import requests
# replace your vercel domain
# replace with your suno-api URL
base_url = 'http://localhost:3000'
@ -208,7 +233,7 @@ if __name__ == '__main__':
```
### Js
### JavaScript
```js
const axios = require("axios");
@ -304,18 +329,18 @@ You can integrate Suno AI as a tool/plugin/action into your AI agent.
There are four ways you can support this project:
1. Fork and Submit Pull Requests: We welcome any PRs that enhance the component or editor.
1. Fork and Submit Pull Requests: We welcome any PRs that enhances the functionality, APIs, response time and availability. You can also help us just by translating this README into your language—any help for this project is welcome!
2. Open Issues: We appreciate reasonable suggestions and bug reports.
3. Donate: If this project has helped you, consider buying us a coffee using the Sponsor button at the top of the project. Cheers! ☕
4. Spread the Word: Recommend this project to others, star the repo, or add a backlink after using the project.
## Questions, Suggestions, Issues, or Bugs?
We use GitHub Issues to manage feedback. Feel free to open an issue, and we'll address it promptly.
We use [GitHub Issues](https://github.com/gcui-art/suno-api/issues) to manage feedback. Feel free to open an issue, and we'll address it promptly.
## License
LGPL-3.0 or later
The license of this project is LGPL-3.0 or later. See [LICENSE](LICENSE) for more information.
## Related Links

View File

@ -8,9 +8,10 @@
<p align="center">
<a target="_blank" href="./README.md">English</a>
| <a target="_blank" href="./README_CN.md">简体中文</a>
| <a target="_blank" href="./README_RU.md">русский</a>
| <a target="_blank" href="https://suno.gcui.ai">Demo</a>
| <a target="_blank" href="https://suno.gcui.ai/docs">文档</a>
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api">一键部署到 Vercel</a>
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api">一键部署到 Vercel</a>
</p>
<p align="center">
@ -61,7 +62,7 @@ Suno.ai v3 是一个令人惊叹的 AI 音乐服务,虽然官方还没有开
#### 部署到 Vercel
[![部署到 Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE&project-name=suno-api&repository-name=suno-api)
[![部署到 Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api)
#### 本地运行

355
README_RU.md Normal file
View File

@ -0,0 +1,355 @@
<div align="center">
<h1 align="center"">
Suno AI API
</h1>
<p>Используйте API для генерации музыки через Suno.ai и с лёгкостью интегрируйте его в агенты, такие как GPT.</p>
<p>👉 Мы обновляемся быстро, пожалуйста, поставьте звёздочку.</p>
</div>
<p align="center">
<a target="_blank" href="./README.md">English</a>
| <a target="_blank" href="./README_CN.md">简体中文</a>
| <a target="_blank" href="./README_RU.md">русский</a>
| <a target="_blank" href="https://suno.gcui.ai">Демо</a>
| <a target="_blank" href="https://suno.gcui.ai/docs">Документация</a>
| <a target="_blank" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api">Развёртывание на Vercel</a>
</p>
<p align="center">
<a href="https://www.producthunt.com/products/gcui-art-suno-api-open-source-sunoai-api/reviews?utm_source=badge-product_review&utm_medium=badge&utm_souce=badge-gcui&#0045;art&#0045;suno&#0045;api&#0045;open&#0045;source&#0045;sunoai&#0045;api" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/product_review.svg?product_id=577408&theme=light" alt="gcui&#0045;art&#0047;suno&#0045;api&#0058;Open&#0045;source&#0032;SunoAI&#0032;API - Use&#0032;API&#0032;to&#0032;call&#0032;the&#0032;music&#0032;generation&#0032;AI&#0032;of&#0032;suno&#0046;ai&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
> 🔥 Посмотрите мой новый проект: [ReadPo — 10-кратное ускорение чтения и печатания](https://readpo.com?utm_source=github&utm_medium=suno-ai)
![Баннер suno-api](https://github.com/gcui-art/suno-api/blob/main/public/suno-banner.png)
## Вступление
Suno — потрясающий сервис для ИИ-музыки. Несмотря на отстутствие официального API, мы не могли дождаться, чтобы интегрировать его возможности где-нибудь.
Мы узнали, что у других пользователей есть схожие потребности, поэтому решили выложить этот проект в открытый доступ, надеясь, что он вам понравится.
Данная реализация использует платный сервис [2Captcha](https://2captcha.com/about) (a.k.a. ruCaptcha) для автоматического решения капч hCaptcha и не использует какие-либо готовые реализации API Suno с закрытым исходным кодом.
## Демо
Мы опубликовали пример, привязанный к бесплатному аккаунту, так что имеются дневные лимиты, но вы всё равно можете посмотреть, как оно работает:
[suno.gcui.ai](https://suno.gcui.ai)
## Функции
- Идеально реализует API suno.ai.
- Автоматическое поддержание сессии аккаунта.
- Автоматическое решение капч через [ruCaptcha](https://rucaptcha.com/about) и [Playwright](https://playwright.dev) с патчами [rebrowser-patches](https://github.com/rebrowser/rebrowser-patches).
- Совместим с форматом API OpenAI `/v1/chat/completions`.
- Поддержка пользовательского текста песни.
- Развёртывание в один клик через [Vercel](#развёртывание-на-vercel) и [Docker](#docker).
- В дополнение к стандартному API, он также адаптируется к схеме API агентских платформ, таких как GPT и Coze, поэтому вы можете использовать его как инструмент/плагин/действие для LLM и интегрировать его в любой AI-агент.
- Разрешительная лицензия с открытым исходным кодом, позволяющая свободно интегрировать и модифицировать.
## Начало работы
### 1. Получите куки вашего аккаунта Suno
1. Зайдите на [suno.com/create](https://suno.com/create).
2. Откройте консоль браузера: нажмите `F12` или откройте инструменты разработчика.
3. Перейдите на вкладку `Сеть` (`Network`).
4. Перезагрузите страницу.
5. Найдите запрос, адрес которого содержит `client?__clerk_api_version`.
6. Нажмите на него и перейдите на вкладку `Заголовки` (`Header`).
7. Найдите заголовок `Cookie`, нажмите ПКМ по нему и скопируйте его значение.
![Видеоинструкция о том, как получить куки](https://github.com/gcui-art/suno-api/blob/main/public/get-cookie-demo.gif)
### 2. Зарегистрируйтесь на 2Captcha и пополните баланс
[2Captcha](https://2captcha.com/ru/about) — это платный сервис для решения капч, использующий реальных работников для этого и обладающий высокой точностью. Он необходим из-за того, что Suno постоянно запрашивает решение hCaptcha, что невозможно за бесплатно каким-либо автоматическим способом.
[Создайте](https://2captcha.com/ru/auth/register?userType=customer) новый аккаунт, [пополните](https://2captcha.com/ru/pay) баланс и [получите свой API-ключ](https://2captcha.com/ru/enterpage#recognition).
Если вы находитесь в России или Беларуси, используйте интерфейс [ruCaptcha](https://rucaptcha.com) вместо 2Captcha. Это абсолютно тот же сервис, но данный интерфейс поддерживает платежи из этих стран.
### 3. Скачайте и запустите проект
Вы можете выбрать свой предпочитаемый способ запуска:
#### Развёртывание на Vercel
[![Развёртывание на Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fgcui-art%2Fsuno-api&env=SUNO_COOKIE,TWOCAPTCHA_KEY,BROWSER,BROWSER_GHOST_CURSOR,BROWSER_LOCALE,BROWSER_HEADLESS&project-name=suno-api&repository-name=suno-api)
#### Локально
```bash
git clone https://github.com/gcui-art/suno-api.git
cd suno-api
npm install
npx playwright install chromium
```
#### Docker
Также можно использовать [Docker Compose](https://docs.docker.com/compose/), однако перед запуском выполните шаг ниже.
```bash
docker compose build && docker compose up
```
### 4. Настройте suno-api
- Если вы используете Vercel, настройте переменные среды в панели управления Vercel.
- Если вы установили suno-api локально, добавьте следующее в ваш `.env`-файл:
#### Environment variables
- `SUNO_COOKIE` — заголовок `Cookie`, который вы скопировали ещё в первом шаге.
- `TWOCAPTCHA_KEY` — ваш API-ключ 2Captcha из второго шага.
- `BROWSER` — название браузера, который будет использоваться для решения капч. Поддерживаются только `chromium` и `firefox`.
- `BROWSER_GHOST_CURSOR` — использовать ли ghost-cursor-playwright для симуляции плавных движений мышкой. Обратите внимание, что это, похоже, никак не влияет на появление капч, так что вы можете спокойно установить значение `false`.
- `BROWSER_LOCALE` — язык браузера. Рекомендуется использовать либо `en`, либо `ru`, т. к. данные языки имеют больше всего работников на 2Captcha. [Список поддерживаемых языков](https://2captcha.com/ru/2captcha-api#language)
- `BROWSER_HEADLESS` — запускать ли браузер без отдельного окна. Скорее всего, вам надо установить значение `true`.
```bash
SUNO_COOKIE=<…>
TWOCAPTCHA_KEY=<…>
BROWSER=chromium
BROWSER_GHOST_CURSOR=false
BROWSER_LOCALE=en
BROWSER_HEADLESS=true
```
### 5. Запустите suno-api
- Если вы используете Vercel:
- Нажмите на кнопку `Deploy` в панели Vercel и дождитесь успеха.
- Посетите API `https://<присовенный-домен-vercel>/api/get_limit` для тестирования.
- Если вы установили проект локально:
- Выполните `npm run dev`.
- Посетите API `http://localhost:3000/api/get_limit` для тестирования.
- Если вернулся следующий результат:
```json
{
"credits_left": 50,
"period": "day",
"monthly_limit": 50,
"monthly_usage": 50
}
```
то программа работает корректно.
### 6. Используйте Suno API
Вы можете посмотреть документацию suno-api здесь:
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
## Справочник по API
На данный момент suno-api реализует следующие API:
```bash
- `/api/generate`: Сгенерировать музыку
- `/v1/chat/completions`: Сгенерировать музыку - Вызов API в формате OpenAI.
- `/api/custom_generate`: Сгенерировать музыку (Custom Mode, поддержка ручного текста песни, стиля музыки, названия и т. д.)
- `/api/generate_lyrics`: Сгенерировать текст песни на основе промпта
- `/api/get`: Получить информацию песни по ID. Перечисляйте несколько ID через запятую.
Если ID не предоставлен, то отобразятся все песни.
- `/api/get_limit`: Получить лимиты на сегодня
- `/api/extend_audio`: Расширить длину песни
- `/api/generate_stems`: Создать стем-треки (отдельную звуковую и музыкальную дорожку)
- `/api/get_aligned_lyrics`: Получить список временных меток для каждого слова в тексте песни
- `/api/clip`: Получить информацию о клипе на основе идентификатора, переданного в качестве параметра запроса `id`.
- `/api/concat`: Сгенерировать всю песню из расширений
```
Вы также можете указать куки в заголовок `Cookie` вашего запроса, переопределяя дефолтные куки в переменной среды `SUNO_COOKIE`. Это удобно, например, когда вы хотите использовать несколько бесплатных аккаунтов одновременно.
Для более подробной документации посетите демо-сайт:
[suno.gcui.ai/docs](https://suno.gcui.ai/docs)
## Пример кода интеграции API
### Python
```python
import time
import requests
# замените на URL-адрес вашего suno-api
base_url = 'http://localhost:3000'
def custom_generate_audio(payload):
url = f"{base_url}/api/custom_generate"
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
return response.json()
def extend_audio(payload):
url = f"{base_url}/api/extend_audio"
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
return response.json()
def generate_audio_by_prompt(payload):
url = f"{base_url}/api/generate"
response = requests.post(url, json=payload, headers={'Content-Type': 'application/json'})
return response.json()
def get_audio_information(audio_ids):
url = f"{base_url}/api/get?ids={audio_ids}"
response = requests.get(url)
return response.json()
def get_quota_information():
url = f"{base_url}/api/get_limit"
response = requests.get(url)
return response.json()
def get_clip(clip_id):
url = f"{base_url}/api/clip?id={clip_id}"
response = requests.get(url)
return response.json()
def generate_whole_song(clip_id):
payload = {"clip_id": clip_id}
url = f"{base_url}/api/concat"
response = requests.post(url, json=payload)
return response.json()
if __name__ == '__main__':
data = generate_audio_by_prompt({
"prompt": "Популярная хэви-метал песня о войне, исполненная глубоким мужским голосом, медленно и мелодично. В тексте изображена печаль людей после войны.",
"make_instrumental": False,
"wait_audio": False
})
ids = f"{data[0]['id']},{data[1]['id']}"
print(f"ids: {ids}")
for _ in range(60):
data = get_audio_information(ids)
if data[0]["status"] == 'streaming':
print(f"{data[0]['id']} ==> {data[0]['audio_url']}")
print(f"{data[1]['id']} ==> {data[1]['audio_url']}")
break
# sleep 5s
time.sleep(5)
```
### JavaScript
```js
const axios = require("axios");
// замените на URL-адрес вашего suno-api
const baseUrl = "http://localhost:3000";
async function customGenerateAudio(payload) {
const url = `${baseUrl}/api/custom_generate`;
const response = await axios.post(url, payload, {
headers: { "Content-Type": "application/json" },
});
return response.data;
}
async function generateAudioByPrompt(payload) {
const url = `${baseUrl}/api/generate`;
const response = await axios.post(url, payload, {
headers: { "Content-Type": "application/json" },
});
return response.data;
}
async function extendAudio(payload) {
const url = `${baseUrl}/api/extend_audio`;
const response = await axios.post(url, payload, {
headers: { "Content-Type": "application/json" },
});
return response.data;
}
async function getAudioInformation(audioIds) {
const url = `${baseUrl}/api/get?ids=${audioIds}`;
const response = await axios.get(url);
return response.data;
}
async function getQuotaInformation() {
const url = `${baseUrl}/api/get_limit`;
const response = await axios.get(url);
return response.data;
}
async function getClipInformation(clipId) {
const url = `${baseUrl}/api/clip?id=${clipId}`;
const response = await axios.get(url);
return response.data;
}
async function main() {
const data = await generateAudioByPrompt({
prompt:
"Популярная хэви-метал песня о войне, исполненная глубоким мужским голосом, медленно и мелодично. В тексте изображена печаль людей после войны.",
make_instrumental: false,
wait_audio: false,
});
const ids = `${data[0].id},${data[1].id}`;
console.log(`ids: ${ids}`);
for (let i = 0; i < 60; i++) {
const data = await getAudioInformation(ids);
if (data[0].status === "streaming") {
console.log(`${data[0].id} ==> ${data[0].audio_url}`);
console.log(`${data[1].id} ==> ${data[1].audio_url}`);
break;
}
// sleep 5s
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
main();
```
## Интеграция с пользовательскими агентами
Вы можете интегрировать Suno AI как инструмент/плагин/действие в ваш ИИ-агент.
### Интеграция с GPT
[скоро...]
### Интеграция с Coze
[скоро...]
### Интеграция с LangChain
[скоро...]
## Вклад в развитие
Вы можете поддержать этот проект четырьмя способами:
1. Fork и публикация pull request'ов: мы приветствуем любые PR, которые улучшают данный проект. Вы также можете помочь простым переводом этого README на ваш язык.
2. Создавайте [issue](https://github.com/gcui-art/suno-api/issues): мы ценим разумные предложения и сообщения об ошибках.
3. Пожертвование: если этот проект помог вам, угостите нас кофе, воспользовавшись кнопкой «Sponsor» в верхней части проекта. Спасибо! ☕
4. Распространяйте информацию: порекомендуйте этот проект другим, поставьте звезду в репо или добавьте обратную ссылку после использования проекта.
## Вопросы, предложения, проблемы или ошибки?
Мы используем [Issues на GitHub](https://github.com/gcui-art/suno-api/issues) для обратной связи. Не стестняйтесь создавать issue, мы оперативно решим вашу проблему.
## Лицензия
Лицензия данного проекта — LGPL-3.0 или более поздняя версия. Для большей информации см. [LICENSE](LICENSE).
## Полезные ссылки
- Репозиторий проекта: [github.com/gcui-art/suno-api](https://github.com/gcui-art/suno-api)
- Официальный сайт Suno.ai: [suno.ai](https://suno.ai)
- Демо: [suno.gcui.ai](https://suno.gcui.ai)
- [Readpo](https://readpo.com?utm_source=github&utm_medium=suno-api): ReadPo — это помощник для чтения и письма, работающий на основе искусственного интеллекта. Собирайте, курируйте и создавайте контент с молниеносной скоростью.
- Album AI: [Автоматическое создание метаданных изображения и общение с альбомом. RAG + Альбом.](https://github.com/gcui-art/album-ai)
## Заявление
suno-api — это неофициальный проект с открытым исходным кодом, предназначенный только для учебных и исследовательских целей.

View File

@ -1,4 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
webpack: (config) => {
config.module.rules.push({
test: /\.(ttf|html)$/i,
type: 'asset/resource'
});
return config;
},
experimental: {
serverMinification: false, // the server minification unfortunately breaks the selector class names
},
};
export default nextConfig;

10008
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,15 @@
"lint": "next lint"
},
"dependencies": {
"@2captcha/captcha-solver": "^1.3.0",
"@vercel/analytics": "^1.2.2",
"axios": "^1.6.8",
"axios-cookiejar-support": "^5.0.0",
"axios": "^1.7.8",
"bufferutil": "^4.0.8",
"chromium-bidi": "^0.10.1",
"cookie": "^1.0.2",
"electron": "^33.2.1",
"ghost-cursor-playwright": "^2.1.0",
"js-cookie": "^3.0.5",
"next": "14.1.4",
"next-swagger-doc": "^0.4.0",
"pino": "^8.19.0",
@ -25,12 +31,16 @@
"react": "^18",
"react-dom": "^18",
"react-markdown": "^9.0.1",
"swagger-ui-react": "^5.12.3",
"rebrowser-playwright-core": "^1.49.1",
"swagger-ui-react": "^5.18.2",
"tough-cookie": "^4.1.4",
"user-agents": "^1.1.156"
"user-agents": "^1.1.156",
"utf-8-validate": "^6.0.5",
"yn": "^5.0.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.12",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",

View File

@ -19,7 +19,7 @@ export async function GET(req: NextRequest) {
});
}
const audioInfo = await (await sunoApi).getClip(clipId);
const audioInfo = await (await sunoApi()).getClip(clipId);
return new NextResponse(JSON.stringify(audioInfo), {
status: 200,

View File

@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@ -18,7 +19,7 @@ export async function POST(req: NextRequest) {
}
});
}
const audioInfo = await (await sunoApi).concatenate(clip_id);
const audioInfo = await (await sunoApi((await cookies()).toString())).concatenate(clip_id);
return new NextResponse(JSON.stringify(audioInfo), {
status: 200,
headers: {

View File

@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers';
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { prompt, tags, title, make_instrumental, model, wait_audio, negative_tags } = body;
const audioInfo = await (await sunoApi).custom_generate(
const audioInfo = await (await sunoApi((await cookies()).toString())).custom_generate(
prompt, tags, title,
Boolean(make_instrumental),
model || DEFAULT_MODEL,
@ -25,18 +26,9 @@ export async function POST(req: NextRequest) {
}
});
} catch (error: any) {
console.error('Error generating custom audio:', error.response.data);
if (error.response.status === 402) {
return new NextResponse(JSON.stringify({ error: error.response.data.detail }), {
status: 402,
headers: {
'Content-Type': 'application/json',
...corsHeaders
}
});
}
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
console.error('Error generating custom audio:', error);
return new NextResponse(JSON.stringify({ error: error.response?.data?.detail || error.toString() }), {
status: error.response?.status || 500,
headers: {
'Content-Type': 'application/json',
...corsHeaders

View File

@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@ -8,7 +9,7 @@ export async function POST(req: NextRequest) {
if (req.method === 'POST') {
try {
const body = await req.json();
const { audio_id, prompt, continue_at, tags, title, model } = body;
const { audio_id, prompt, continue_at, tags, negative_tags, title, model, wait_audio } = body;
if (!audio_id) {
return new NextResponse(JSON.stringify({ error: 'Audio ID is required' }), {
@ -20,8 +21,8 @@ export async function POST(req: NextRequest) {
});
}
const audioInfo = await (await sunoApi)
.extendAudio(audio_id, prompt, continue_at, tags, title, model || DEFAULT_MODEL);
const audioInfo = await (await sunoApi((await cookies()).toString()))
.extendAudio(audio_id, prompt, continue_at, tags || '', negative_tags || '', title, model || DEFAULT_MODEL, wait_audio || false);
return new NextResponse(JSON.stringify(audioInfo), {
status: 200,

View File

@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
const body = await req.json();
const { prompt, make_instrumental, model, wait_audio } = body;
const audioInfo = await (await sunoApi).generate(
const audioInfo = await (await sunoApi((await cookies()).toString())).generate(
prompt,
Boolean(make_instrumental),
model || DEFAULT_MODEL,

View File

@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@ -10,7 +11,7 @@ export async function POST(req: NextRequest) {
const body = await req.json();
const { prompt } = body;
const lyrics = await (await sunoApi).generateLyrics(prompt);
const lyrics = await (await sunoApi((await cookies()).toString())).generateLyrics(prompt);
return new NextResponse(JSON.stringify(lyrics), {
status: 200,

View File

@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers';
import { DEFAULT_MODEL, sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@ -20,7 +21,7 @@ export async function POST(req: NextRequest) {
});
}
const audioInfo = await (await sunoApi)
const audioInfo = await (await sunoApi((await cookies()).toString()))
.generateStems(audio_id);
return new NextResponse(JSON.stringify(audioInfo), {

View File

@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { sunoApi } from '@/lib/SunoApi';
import { corsHeaders } from '@/lib/utils';
@ -10,13 +11,14 @@ export async function GET(req: NextRequest) {
const url = new URL(req.url);
const songIds = url.searchParams.get('ids');
const page = url.searchParams.get('page');
const cookie = (await cookies()).toString();
let audioInfo = [];
if (songIds && songIds.length > 0) {
const idsArray = songIds.split(',');
audioInfo = await (await sunoApi).get(idsArray, page);
audioInfo = await (await sunoApi(cookie)).get(idsArray, page);
} else {
audioInfo = await (await sunoApi).get(undefined, page);
audioInfo = await (await sunoApi(cookie)).get(undefined, page);
}
return new NextResponse(JSON.stringify(audioInfo), {

View File

@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@ -20,7 +21,7 @@ export async function GET(req: NextRequest) {
});
}
const lyricAlignment = await (await sunoApi).getLyricAlignment(song_id);
const lyricAlignment = await (await sunoApi((await cookies()).toString())).getLyricAlignment(song_id);
return new NextResponse(JSON.stringify(lyricAlignment), {

View File

@ -1,4 +1,5 @@
import { NextResponse, NextRequest } from "next/server";
import { cookies } from 'next/headers'
import { sunoApi } from "@/lib/SunoApi";
import { corsHeaders } from "@/lib/utils";
@ -8,7 +9,7 @@ export async function GET(req: NextRequest) {
if (req.method === 'GET') {
try {
const limit = await (await sunoApi).get_credits();
const limit = await (await sunoApi((await cookies()).toString())).get_credits();
return new NextResponse(JSON.stringify(limit), {

View File

@ -9,7 +9,12 @@ type Props = {
const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false });
function Swagger({ spec }: Props) {
return <SwaggerUI spec={spec}/>;
return <SwaggerUI spec={spec} requestInterceptor={(req) => {
// Remove cookies before sending requests
req.credentials = 'omit';
console.log(req);
return req;
}} />;
}
export default Swagger;

View File

@ -231,6 +231,11 @@
"description": "Music genre",
"example": ""
},
"negative_tags": {
"type": "string",
"description": "Negative Music genre",
"example":""
},
"model": {
"type": "string",
"description": "Model name ,default is chirp-v3-5",

View File

@ -32,7 +32,7 @@ export async function POST(req: NextRequest) {
}
const audioInfo = await (await sunoApi).generate(userMessage.content, true, DEFAULT_MODEL, true);
const audioInfo = await (await sunoApi()).generate(userMessage.content, true, DEFAULT_MODEL, true);
const audio = audioInfo[0]
const data = `## Song Title: ${audio.title}\n![Song Cover](${audio.image_url})\n### Lyrics:\n${audio.lyric}\n### Listen to the song: ${audio.audio_url}`

View File

@ -1,9 +1,18 @@
import axios, { AxiosInstance } from 'axios';
import UserAgent from 'user-agents';
import pino from 'pino';
import { wrapper } from 'axios-cookiejar-support';
import { CookieJar } from 'tough-cookie';
import { sleep } from '@/lib/utils';
import yn from 'yn';
import { sleep, isPage } from '@/lib/utils';
import * as cookie from 'cookie';
import { randomUUID } from 'node:crypto';
import { Solver } from '@2captcha/captcha-solver';
import { BrowserContext, Page, Locator, chromium, firefox } from 'rebrowser-playwright-core';
import { createCursor, Cursor } from 'ghost-cursor-playwright';
// sunoApi instance caching
const globalForSunoApi = global as unknown as { sunoApiCache?: Map<string, SunoApi> };
const cache = globalForSunoApi.sunoApiCache || new Map<string, SunoApi>();
globalForSunoApi.sunoApiCache = cache;
const logger = pino();
export const DEFAULT_MODEL = 'chirp-v3-5';
@ -30,37 +39,58 @@ export interface AudioInfo {
class SunoApi {
private static BASE_URL: string = 'https://studio-api.prod.suno.com';
private static CLERK_BASE_URL: string = 'https://clerk.suno.com';
private static JSDELIVR_BASE_URL: string = 'https://data.jsdelivr.com';
private static CLERK_VERSION = '5.15.0';
private readonly client: AxiosInstance;
private clerkVersion?: string;
private sid?: string;
private currentToken?: string;
private deviceId?: string;
private userAgent?: string;
private cookies: Record<string, string | undefined>;
private solver = new Solver(process.env.TWOCAPTCHA_KEY + '');
private ghostCursorEnabled = yn(process.env.BROWSER_GHOST_CURSOR, { default: false });
private cursor?: Cursor;
constructor(cookie: string) {
const cookieJar = new CookieJar();
const randomUserAgent = new UserAgent(/Chrome/).random().toString();
this.client = wrapper(
axios.create({
jar: cookieJar,
constructor(cookies: string) {
this.userAgent = new UserAgent(/Macintosh/).random().toString(); // Usually Mac systems get less amount of CAPTCHAs
this.cookies = cookie.parse(cookies);
this.deviceId = this.cookies.ajs_anonymous_id || randomUUID();
this.client = axios.create({
withCredentials: true,
headers: {
'User-Agent': randomUserAgent,
Cookie: cookie
'Affiliate-Id': 'undefined',
'Device-Id': `"${this.deviceId}"`,
'x-suno-client': 'Android prerelease-4nt180t 1.0.42',
'X-Requested-With': 'com.suno.android',
'sec-ch-ua': '"Chromium";v="130", "Android WebView";v="130", "Not?A_Brand";v="99"',
'sec-ch-ua-mobile': '?1',
'sec-ch-ua-platform': '"Android"',
'User-Agent': this.userAgent
}
})
});
this.client.interceptors.request.use(config => {
if (this.currentToken && !config.headers.Authorization)
config.headers.Authorization = `Bearer ${this.currentToken}`;
const cookiesArray = Object.entries(this.cookies).map(([key, value]) =>
cookie.serialize(key, value as string)
);
this.client.interceptors.request.use((config) => {
if (this.currentToken) {
// Use the current token status
config.headers['Authorization'] = `Bearer ${this.currentToken}`;
}
config.headers.Cookie = cookiesArray.join('; ');
return config;
});
this.client.interceptors.response.use(resp => {
const setCookieHeader = resp.headers['set-cookie'];
if (Array.isArray(setCookieHeader)) {
const newCookies = cookie.parse(setCookieHeader.join('; '));
for (const [key, value] of Object.entries(newCookies)) {
this.cookies[key] = value;
}
}
return resp;
})
}
public async init(): Promise<SunoApi> {
await this.getClerkLatestVersion();
//await this.getClerkLatestVersion();
await this.getAuthToken();
await this.keepAlive();
return this;
@ -68,7 +98,8 @@ class SunoApi {
/**
* Get the clerk package latest version id.
*/
* This method is commented because we are now using a hard-coded Clerk version, hence this method is not needed.
private async getClerkLatestVersion() {
// URL to get clerk version ID
const getClerkVersionUrl = `${SunoApi.JSDELIVR_BASE_URL}/v1/package/npm/@clerk/clerk-js`;
@ -80,26 +111,28 @@ class SunoApi {
);
}
// Save clerk version ID for auth
// this.clerkVersion = versionListResponse?.data?.['tags']['latest'];
// Use a Clerk version released before fraud detection was implemented
this.clerkVersion = "5.34.0";
SunoApi.clerkVersion = versionListResponse?.data?.['tags']['latest'];
}
*/
/**
* Get the session ID and save it for later use.
*/
private async getAuthToken() {
logger.info('Getting the session ID');
// URL to get session ID
const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_clerk_js_version=${this.clerkVersion}`;
const getSessionUrl = `${SunoApi.CLERK_BASE_URL}/v1/client?_is_native=true&_clerk_js_version=${SunoApi.CLERK_VERSION}`;
// Get session ID
const sessionResponse = await this.client.get(getSessionUrl);
if (!sessionResponse?.data?.response?.['last_active_session_id']) {
const sessionResponse = await this.client.get(getSessionUrl, {
headers: { Authorization: this.cookies.__client }
});
if (!sessionResponse?.data?.response?.last_active_session_id) {
throw new Error(
'Failed to get session id, you may need to update the SUNO_COOKIE'
);
}
// Save session ID for later use
this.sid = sessionResponse.data.response['last_active_session_id'];
this.sid = sessionResponse.data.response.last_active_session_id;
}
/**
@ -111,16 +144,238 @@ class SunoApi {
throw new Error('Session ID is not set. Cannot renew token.');
}
// URL to renew session token
const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens?_clerk_js_version==${this.clerkVersion}`;
const renewUrl = `${SunoApi.CLERK_BASE_URL}/v1/client/sessions/${this.sid}/tokens?_is_native=true&_clerk_js_version=${SunoApi.CLERK_VERSION}`;
// Renew session token
const renewResponse = await this.client.post(renewUrl);
logger.info('KeepAlive...\n');
const renewResponse = await this.client.post(renewUrl, {}, {
headers: { Authorization: this.cookies.__client }
});
if (isWait) {
await sleep(1, 2);
}
const newToken = renewResponse.data['jwt'];
const newToken = renewResponse.data.jwt;
// Update Authorization field in request header with the new JWT token
this.currentToken = newToken;
logger.info(this.currentToken);
}
/**
* Get the session token (not to be confused with session ID) and save it for later use.
*/
private async getSessionToken() {
const tokenResponse = await this.client.post(
`${SunoApi.BASE_URL}/api/user/create_session_id/`,
{
session_properties: JSON.stringify({ deviceId: this.deviceId }),
session_type: 1
}
);
return tokenResponse.data.session_id;
}
private async captchaRequired(): Promise<boolean> {
const resp = await this.client.post(`${SunoApi.BASE_URL}/api/c/check`, {
ctype: 'generation'
});
logger.info(resp.data);
// await sleep(10);
return resp.data.required;
}
/**
* Clicks on a locator or XY vector. This method is made because of the difference between ghost-cursor-playwright and Playwright methods
*/
private async click(target: Locator|Page, position?: { x: number, y: number }): Promise<void> {
if (this.ghostCursorEnabled) {
let pos: any = isPage(target) ? { x: 0, y: 0 } : await target.boundingBox();
if (position)
pos = {
...pos,
x: pos.x + position.x,
y: pos.y + position.y,
width: null,
height: null,
};
return this.cursor?.actions.click({
target: pos
});
} else {
if (isPage(target))
return target.mouse.click(position?.x ?? 0, position?.y ?? 0);
else
return target.click({ force: true, position });
}
}
/**
* Get the BrowserType from the `BROWSER` environment variable.
* @returns {BrowserType} chromium, firefox or webkit. Default is chromium
*/
private getBrowserType() {
const browser = process.env.BROWSER?.toLowerCase();
switch (browser) {
case 'firefox':
return firefox;
/*case 'webkit': ** doesn't work with rebrowser-patches
case 'safari':
return webkit;*/
default:
return chromium;
}
}
/**
* Launches a browser with the necessary cookies
* @returns {BrowserContext}
*/
private async launchBrowser(): Promise<BrowserContext> {
const browser = await this.getBrowserType().launch({
args: [
'--disable-blink-features=AutomationControlled',
'--disable-web-security',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-features=site-per-process',
'--disable-features=IsolateOrigins',
'--disable-extensions',
'--disable-infobars'
],
headless: yn(process.env.BROWSER_HEADLESS, { default: true })
});
const context = await browser.newContext({ userAgent: this.userAgent, locale: process.env.BROWSER_LOCALE, viewport: null });
const cookies = [];
const lax: 'Lax' | 'Strict' | 'None' = 'Lax';
cookies.push({
name: '__session',
value: this.currentToken+'',
domain: '.suno.com',
path: '/',
sameSite: lax
});
for (const key in this.cookies) {
cookies.push({
name: key,
value: this.cookies[key]+'',
domain: '.suno.com',
path: '/',
sameSite: lax
})
}
await context.addCookies(cookies);
return context;
}
/**
* Checks for CAPTCHA verification and solves the CAPTCHA if needed
* @returns {string|null} hCaptcha token. If no verification is required, returns null
*/
public async getCaptcha(): Promise<string|null> {
if (!await this.captchaRequired())
return null;
logger.info('CAPTCHA required. Launching browser...')
const browser = await this.launchBrowser();
const page = await browser.newPage();
await page.goto('https://suno.com/create', { referer: 'https://www.google.com/', waitUntil: 'domcontentloaded', timeout: 0 });
page.on('request', request => console.log('>>', request.method(), request.url()));
page.on('response', response => console.log('<<', response.status(), response.url()));
logger.info('Waiting for Suno interface to load');
await page.locator('.react-aria-GridList').waitFor({ timeout: 60000 });
if (this.ghostCursorEnabled)
this.cursor = await createCursor(page);
logger.info('Triggering the CAPTCHA');
await this.click(page, { x: 318, y: 13 }); // close all popups
const textarea = page.locator('.custom-textarea');
await this.click(textarea);
await textarea.pressSequentially('Lorem ipsum', { delay: 80 });
const button = page.locator('button[aria-label="Create"]').locator('div.flex');
await this.click(button);
new Promise<void>(async (resolve, reject) => {
const frame = page.frameLocator('iframe[title*="hCaptcha"]');
const challenge = frame.locator('.challenge-container');
while (true) {
try {
await page.waitForResponse('https://img**.hcaptcha.com/**', { timeout: 60000 }); // wait for hCaptcha image to load
while (true) { // wait for all requests to finish
try {
await page.waitForResponse('https://img**.hcaptcha.com/**', { timeout: 1000 });
} catch(e) {
break
}
}
//await sleep(0.1); // sometimes it takes a couple of seconds to display the image itself. unfortunately, the only option is to wait and hope that it loads
const drag = (await challenge.locator('.prompt-text').first().innerText()).toLowerCase().includes('drag');
if (drag) {
logger.info('Got a dragging hCaptcha. This type of hCaptcha is currently not supported. Skipping...');
this.click(frame.locator('.button-submit'));
continue;
}
let captcha: any;
for (let j = 0; j < 3; j++) { // try several times because sometimes 2Captcha could send an error
try {
logger.info('Sending the CAPTCHA to 2Captcha');
captcha = await this.solver.coordinates({
body: (await challenge.screenshot()).toString('base64'),
lang: process.env.BROWSER_LOCALE
});
break;
} catch(err: any) {
logger.info(err.message);
if (j != 2)
logger.info('Retrying...');
else
throw err;
}
}
for (const data of captcha.data) {
logger.info(data);
await this.click(challenge, { x: +data.x, y: +data.y });
}
/*await*/ this.click(frame.locator('.button-submit')); // await is commented because we need to call waitForResponse at the same time
} catch(e: any) {
if (e.message.includes('viewport') || e.message.includes('timeout')) // when hCaptcha window has been closed due to inactivity,
this.click(button); // click the Create button again to trigger the CAPTCHA
else if (e.message.includes('been closed')) // catch error when closing the browser
resolve();
else
reject(e);
}
}
}).catch(e => {
//if (!e.message.includes('been closed'))
throw e;
});
return (new Promise((resolve, reject) => {
page.route('**/api/generate/v2/**', async (route: any) => {
try {
logger.info('hCaptcha token received. Closing browser');
route.abort();
browser.browser()?.close();
const request = route.request();
this.currentToken = request.headers().authorization.split('Bearer ').pop();
resolve(request.postDataJSON().token);
} catch(err) {
reject(err);
}
});
}));
}
/**
* Imitates Cloudflare Turnstile loading error. Unused right now, left for future
*/
private async getTurnstile() {
return this.client.post(
`https://clerk.suno.com/v1/client?__clerk_api_version=2021-02-05&_clerk_js_version=${SunoApi.CLERK_VERSION}&_method=PATCH`,
{ captcha_error: '300030,300030,300030' },
{ headers: { 'content-type': 'application/x-www-form-urlencoded' } });
}
/**
@ -225,6 +480,8 @@ class SunoApi {
* @param make_instrumental Indicates if the generated song should be instrumental.
* @param wait_audio Indicates if the method should wait for the audio file to be fully generated before returning.
* @param negative_tags Negative tags that should not be included in the generated audio.
* @param task Optional indication of what to do. Enter 'extend' if extending an audio, otherwise specify null.
* @param continue_clip_id
* @returns A promise that resolves to an array of AudioInfo objects representing the generated songs.
*/
private async generateSongs(
@ -235,14 +492,21 @@ class SunoApi {
make_instrumental?: boolean,
model?: string,
wait_audio: boolean = false,
negative_tags?: string
negative_tags?: string,
task?: string,
continue_clip_id?: string,
continue_at?: number
): Promise<AudioInfo[]> {
await this.keepAlive(false);
await this.keepAlive();
const payload: any = {
make_instrumental: make_instrumental,
mv: model || DEFAULT_MODEL,
prompt: '',
generation_type: 'TEXT'
generation_type: 'TEXT',
continue_at: continue_at,
continue_clip_id: continue_clip_id,
task: task,
token: await this.getCaptcha()
};
if (isCustom) {
payload.tags = tags;
@ -276,13 +540,10 @@ class SunoApi {
timeout: 10000 // 10 seconds timeout
}
);
logger.info(
'generateSongs Response:\n' + JSON.stringify(response.data, null, 2)
);
if (response.status !== 200) {
throw new Error('Error response:' + response.statusText);
}
const songIds = response.data['clips'].map((audio: any) => audio.id);
const songIds = response.data.clips.map((audio: any) => audio.id);
//Want to wait for music file generation
if (wait_audio) {
const startTime = Date.now();
@ -303,8 +564,7 @@ class SunoApi {
}
return lastResponse;
} else {
await this.keepAlive(true);
return response.data['clips'].map((audio: any) => ({
return response.data.clips.map((audio: any) => ({
id: audio.id,
title: audio.title,
image_url: audio.image_url,
@ -366,26 +626,14 @@ class SunoApi {
public async extendAudio(
audioId: string,
prompt: string = '',
continueAt: string = '0',
continueAt: number,
tags: string = '',
negative_tags: string = '',
title: string = '',
model?: string
): Promise<AudioInfo> {
await this.keepAlive(false);
const response = await this.client.post(
`${SunoApi.BASE_URL}/api/generate/v2/`,
{
continue_clip_id: audioId,
continue_at: continueAt,
mv: model || DEFAULT_MODEL,
prompt: prompt,
tags: tags,
task: 'extend',
title: title
}
);
console.log('response\n', response);
return response.data;
model?: string,
wait_audio?: boolean
): Promise<AudioInfo[]> {
return this.generateSongs(prompt, true, tags, title, false, model, wait_audio, negative_tags, 'extend', audioId, continueAt);
}
/**
@ -460,7 +708,7 @@ class SunoApi {
page?: string | null
): Promise<AudioInfo[]> {
await this.keepAlive(false);
let url = new URL(`${SunoApi.BASE_URL}/api/feed/`);
let url = new URL(`${SunoApi.BASE_URL}/api/feed/v2`);
if (songIds) {
url.searchParams.append('ids', songIds.join(','));
}
@ -469,11 +717,11 @@ class SunoApi {
}
logger.info('Get audio status: ' + url.href);
const response = await this.client.get(url.href, {
// 3 seconds timeout
timeout: 3000
// 10 seconds timeout
timeout: 10000
});
const audios = response.data;
const audios = response.data.clips;
return audios.map((audio: any) => ({
id: audio.id,
@ -523,13 +771,22 @@ class SunoApi {
}
}
const newSunoApi = async (cookie: string) => {
const sunoApi = new SunoApi(cookie);
return await sunoApi.init();
};
if (!process.env.SUNO_COOKIE) {
console.log('Environment does not contain SUNO_COOKIE.', process.env);
export const sunoApi = async (cookie?: string) => {
const resolvedCookie = cookie || process.env.SUNO_COOKIE;
if (!resolvedCookie) {
logger.info('No cookie provided! Aborting...\nPlease provide a cookie either in the .env file or in the Cookie header of your request.')
throw new Error('Please provide a cookie either in the .env file or in the Cookie header of your request.');
}
export const sunoApi = newSunoApi(process.env.SUNO_COOKIE || '');
// Check if the instance for this cookie already exists in the cache
const cachedInstance = cache.get(resolvedCookie);
if (cachedInstance)
return cachedInstance;
// If not, create a new instance and initialize it
const instance = await new SunoApi(resolvedCookie).init();
// Cache the initialized instance
cache.set(resolvedCookie, instance);
return instance;
};

View File

@ -1,4 +1,5 @@
import pino from "pino";
import { Page } from "rebrowser-playwright-core";
const logger = pino();
@ -20,6 +21,14 @@ export const sleep = (x: number, y?: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, timeout));
}
/**
* @param target A Locator or a page
* @returns {boolean}
*/
export const isPage = (target: any): target is Page => {
return target.constructor.name === 'Page';
}
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',

View File

@ -5,8 +5,10 @@
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"module": "esnext",
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
@ -18,7 +20,8 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"playwright-core": ["./node_modules/rebrowser-playwright-core"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],