Upload 7 files
Browse files- Dockerfile +11 -4
- cookie.py +94 -0
- deps.py +47 -0
- main.py +125 -0
- requirements.txt +6 -0
- schemas.py +53 -0
- utils.py +75 -0
Dockerfile
CHANGED
@@ -1,5 +1,12 @@
|
|
1 |
-
FROM
|
2 |
|
3 |
-
|
4 |
-
|
5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10-slim-buster
|
2 |
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
COPY requirements.txt ./
|
6 |
+
RUN --mount=type=cache,target=/root/.cache/pip \
|
7 |
+
pip install -r requirements.txt --no-cache-dir
|
8 |
+
|
9 |
+
COPY . .
|
10 |
+
|
11 |
+
EXPOSE 8000
|
12 |
+
CMD [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000" ]
|
cookie.py
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding:utf-8 -*-
|
2 |
+
|
3 |
+
import os
|
4 |
+
import time
|
5 |
+
from http.cookies import SimpleCookie
|
6 |
+
from threading import Thread
|
7 |
+
|
8 |
+
import requests
|
9 |
+
|
10 |
+
from utils import COMMON_HEADERS
|
11 |
+
|
12 |
+
|
13 |
+
class SunoCookie:
|
14 |
+
def __init__(self):
|
15 |
+
self.cookie = SimpleCookie()
|
16 |
+
self.session_id = None
|
17 |
+
self.token = None
|
18 |
+
self.is_disabled = False
|
19 |
+
|
20 |
+
def load_cookie(self, cookie_str):
|
21 |
+
self.cookie.load(cookie_str)
|
22 |
+
|
23 |
+
def get_cookie(self):
|
24 |
+
return ";".join([f"{i}={self.cookie.get(i).value}" for i in self.cookie.keys()])
|
25 |
+
|
26 |
+
def set_session_id(self, session_id):
|
27 |
+
self.session_id = session_id
|
28 |
+
|
29 |
+
def get_session_id(self):
|
30 |
+
return self.session_id
|
31 |
+
|
32 |
+
def get_token(self):
|
33 |
+
return self.token
|
34 |
+
|
35 |
+
def set_token(self, token: str):
|
36 |
+
self.token = token
|
37 |
+
|
38 |
+
def disable(self):
|
39 |
+
self.is_disabled = True
|
40 |
+
|
41 |
+
def enable(self):
|
42 |
+
self.is_disabled = False
|
43 |
+
|
44 |
+
def load_env_cookies():
|
45 |
+
suno_auths = {}
|
46 |
+
for key, value in os.environ.items():
|
47 |
+
if key.startswith("SESSION_ID"):
|
48 |
+
try:
|
49 |
+
i = int(key[10:]) # 提取 SESSION_ID 后的数字
|
50 |
+
cookie = os.getenv(f"COOKIE{i}")
|
51 |
+
if cookie:
|
52 |
+
suno_auth = SunoCookie()
|
53 |
+
suno_auth.set_session_id(value)
|
54 |
+
suno_auth.load_cookie(cookie)
|
55 |
+
suno_auths[i] = suno_auth
|
56 |
+
except ValueError:
|
57 |
+
continue
|
58 |
+
return suno_auths
|
59 |
+
|
60 |
+
def update_token(suno_cookie: SunoCookie):
|
61 |
+
headers = {"cookie": suno_cookie.get_cookie()}
|
62 |
+
headers.update(COMMON_HEADERS)
|
63 |
+
session_id = suno_cookie.get_session_id()
|
64 |
+
|
65 |
+
resp = requests.post(
|
66 |
+
url=f"https://clerk.suno.com/v1/client/sessions/{session_id}/tokens?_clerk_js_version=5.22.3",
|
67 |
+
headers=headers,
|
68 |
+
)
|
69 |
+
|
70 |
+
resp_headers = dict(resp.headers)
|
71 |
+
set_cookie = resp_headers.get("Set-Cookie")
|
72 |
+
suno_cookie.load_cookie(set_cookie)
|
73 |
+
token = resp.json().get("jwt")
|
74 |
+
suno_cookie.set_token(token)
|
75 |
+
# print(set_cookie)
|
76 |
+
# print(f"*** token -> {token} ***")
|
77 |
+
|
78 |
+
|
79 |
+
def keep_alive(suno_cookie: SunoCookie):
|
80 |
+
while True:
|
81 |
+
try:
|
82 |
+
update_token(suno_cookie)
|
83 |
+
except Exception as e:
|
84 |
+
print(e)
|
85 |
+
finally:
|
86 |
+
time.sleep(5)
|
87 |
+
|
88 |
+
def start_keep_alive(suno_auths):
|
89 |
+
for suno_auth in suno_auths.values():
|
90 |
+
t = Thread(target=keep_alive, args=(suno_auth,))
|
91 |
+
t.start()
|
92 |
+
|
93 |
+
suno_auths = load_env_cookies()
|
94 |
+
start_keep_alive(suno_auths)
|
deps.py
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding:utf-8 -*-
|
2 |
+
|
3 |
+
from cookie import suno_auths
|
4 |
+
from utils import get_credits
|
5 |
+
import logging
|
6 |
+
import asyncio
|
7 |
+
|
8 |
+
async def get_token():
|
9 |
+
disabled_accounts = set()
|
10 |
+
|
11 |
+
while True:
|
12 |
+
for i, suno_auth in sorted(suno_auths.items()):
|
13 |
+
if suno_auth.is_disabled or i in disabled_accounts:
|
14 |
+
continue
|
15 |
+
|
16 |
+
token = suno_auth.get_token()
|
17 |
+
|
18 |
+
try:
|
19 |
+
credits_info = await get_credits(token)
|
20 |
+
credits = credits_info.get('credits_left', 0)
|
21 |
+
logging.info(f"当前账号 {suno_auth.get_session_id()} 积分: {credits}")
|
22 |
+
|
23 |
+
if credits > 5:
|
24 |
+
return token
|
25 |
+
except Exception as e:
|
26 |
+
logging.error(f"账号 {suno_auth.get_session_id()} 获取积分失败: {e}")
|
27 |
+
suno_auth.disable()
|
28 |
+
disabled_accounts.add(i)
|
29 |
+
|
30 |
+
if len(disabled_accounts) == len(suno_auths):
|
31 |
+
logging.warning("所有账号都已被禁用,等待 1 小时后重试...")
|
32 |
+
await asyncio.sleep(3600) # 等待1小时 (3600秒)
|
33 |
+
# 重置所有账号的禁用状态
|
34 |
+
for suno_auth in suno_auths.values():
|
35 |
+
suno_auth.enable()
|
36 |
+
disabled_accounts.clear()
|
37 |
+
else:
|
38 |
+
logging.warning("所有可用账号积分已用尽,等待 1 小时后重试...")
|
39 |
+
await asyncio.sleep(3600) # 等待1小时 (3600秒)
|
40 |
+
|
41 |
+
# 在程序启动时重置所有账号的禁用状态
|
42 |
+
def reset_account_status():
|
43 |
+
for suno_auth in suno_auths.values():
|
44 |
+
suno_auth.enable()
|
45 |
+
|
46 |
+
# 确保在主程序中调用此函数
|
47 |
+
reset_account_status()
|
main.py
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding:utf-8 -*-
|
2 |
+
|
3 |
+
import json
|
4 |
+
import logging
|
5 |
+
from fastapi import Depends, FastAPI, HTTPException, Request, status
|
6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
7 |
+
|
8 |
+
import schemas
|
9 |
+
from deps import get_token, reset_account_status
|
10 |
+
from utils import generate_lyrics, generate_music, get_feed, get_lyrics, get_credits
|
11 |
+
from cookie import suno_auths
|
12 |
+
|
13 |
+
app = FastAPI()
|
14 |
+
|
15 |
+
app.add_middleware(
|
16 |
+
CORSMiddleware,
|
17 |
+
allow_origins=["*"],
|
18 |
+
allow_credentials=True,
|
19 |
+
allow_methods=["*"],
|
20 |
+
allow_headers=["*"],
|
21 |
+
)
|
22 |
+
|
23 |
+
# 配置日志
|
24 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
25 |
+
|
26 |
+
@app.on_event("startup")
|
27 |
+
async def startup_event():
|
28 |
+
# 在应用启动时重置所有账号状态
|
29 |
+
reset_account_status()
|
30 |
+
logging.info("所有账号状态已重置")
|
31 |
+
|
32 |
+
@app.get("/")
|
33 |
+
async def get_root():
|
34 |
+
return schemas.Response()
|
35 |
+
|
36 |
+
@app.post("/generate")
|
37 |
+
async def generate(
|
38 |
+
data: schemas.CustomModeGenerateParam, token: str = Depends(get_token)
|
39 |
+
):
|
40 |
+
try:
|
41 |
+
resp = await generate_music(data.dict(), token)
|
42 |
+
return resp
|
43 |
+
except Exception as e:
|
44 |
+
logging.error(f"生成音乐失败: {str(e)}")
|
45 |
+
raise HTTPException(
|
46 |
+
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
47 |
+
)
|
48 |
+
|
49 |
+
@app.post("/generate/description-mode")
|
50 |
+
async def generate_with_song_description(
|
51 |
+
data: schemas.DescriptionModeGenerateParam, token: str = Depends(get_token)
|
52 |
+
):
|
53 |
+
try:
|
54 |
+
resp = await generate_music(data.dict(), token)
|
55 |
+
return resp
|
56 |
+
except Exception as e:
|
57 |
+
logging.error(f"根据描述生成音乐失败: {str(e)}")
|
58 |
+
raise HTTPException(
|
59 |
+
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
60 |
+
)
|
61 |
+
|
62 |
+
@app.get("/feed/{aid}")
|
63 |
+
async def fetch_feed(aid: str, token: str = Depends(get_token)):
|
64 |
+
try:
|
65 |
+
resp = await get_feed(aid, token)
|
66 |
+
return resp
|
67 |
+
except Exception as e:
|
68 |
+
logging.error(f"获取feed失败: {str(e)}")
|
69 |
+
raise HTTPException(
|
70 |
+
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
71 |
+
)
|
72 |
+
|
73 |
+
@app.post("/generate/lyrics/")
|
74 |
+
async def generate_lyrics_post(request: Request, token: str = Depends(get_token)):
|
75 |
+
req = await request.json()
|
76 |
+
prompt = req.get("prompt")
|
77 |
+
if prompt is None:
|
78 |
+
raise HTTPException(
|
79 |
+
detail="prompt is required", status_code=status.HTTP_400_BAD_REQUEST
|
80 |
+
)
|
81 |
+
|
82 |
+
try:
|
83 |
+
resp = await generate_lyrics(prompt, token)
|
84 |
+
return resp
|
85 |
+
except Exception as e:
|
86 |
+
logging.error(f"生成歌词失败: {str(e)}")
|
87 |
+
raise HTTPException(
|
88 |
+
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
89 |
+
)
|
90 |
+
|
91 |
+
@app.get("/lyrics/{lid}")
|
92 |
+
async def fetch_lyrics(lid: str, token: str = Depends(get_token)):
|
93 |
+
try:
|
94 |
+
resp = await get_lyrics(lid, token)
|
95 |
+
return resp
|
96 |
+
except Exception as e:
|
97 |
+
logging.error(f"获取歌词失败: {str(e)}")
|
98 |
+
raise HTTPException(
|
99 |
+
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
100 |
+
)
|
101 |
+
|
102 |
+
@app.get("/get_credits")
|
103 |
+
async def fetch_credits(token: str = Depends(get_token)):
|
104 |
+
try:
|
105 |
+
resp = await get_credits(token)
|
106 |
+
return resp
|
107 |
+
except Exception as e:
|
108 |
+
logging.error(f"获取积分失败: {str(e)}")
|
109 |
+
raise HTTPException(
|
110 |
+
detail=str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
111 |
+
)
|
112 |
+
|
113 |
+
@app.get("/account_status")
|
114 |
+
async def get_account_status():
|
115 |
+
status = {}
|
116 |
+
for i, suno_auth in suno_auths.items():
|
117 |
+
status[i] = {
|
118 |
+
"session_id": suno_auth.get_session_id(),
|
119 |
+
"is_disabled": suno_auth.is_disabled
|
120 |
+
}
|
121 |
+
return status
|
122 |
+
|
123 |
+
if __name__ == "__main__":
|
124 |
+
import uvicorn
|
125 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
aiohttp
|
2 |
+
python-dotenv
|
3 |
+
fastapi
|
4 |
+
uvicorn
|
5 |
+
pydantic
|
6 |
+
requests
|
schemas.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding:utf-8 -*-
|
2 |
+
|
3 |
+
from datetime import datetime
|
4 |
+
from typing import Any, List, Optional, Union
|
5 |
+
|
6 |
+
from pydantic import BaseModel, Field
|
7 |
+
|
8 |
+
|
9 |
+
class Response(BaseModel):
|
10 |
+
code: Optional[int] = 0
|
11 |
+
msg: Optional[str] = "success"
|
12 |
+
data: Optional[Any] = None
|
13 |
+
|
14 |
+
|
15 |
+
class CustomModeGenerateParam(BaseModel):
|
16 |
+
"""Generate with Custom Mode"""
|
17 |
+
|
18 |
+
prompt: str = Field(..., description="lyrics")
|
19 |
+
mv: str = Field(
|
20 |
+
...,
|
21 |
+
description="model version, default: chirp-v3-0",
|
22 |
+
examples=["chirp-v3-0"],
|
23 |
+
)
|
24 |
+
title: str = Field(..., description="song title")
|
25 |
+
tags: str = Field(..., description="style of music")
|
26 |
+
continue_at: Optional[int] = Field(
|
27 |
+
default=None,
|
28 |
+
description="continue a new clip from a previous song, format number",
|
29 |
+
examples=[120],
|
30 |
+
)
|
31 |
+
continue_clip_id: Optional[str] = None
|
32 |
+
|
33 |
+
negative_tags: str = ""
|
34 |
+
|
35 |
+
|
36 |
+
class DescriptionModeGenerateParam(BaseModel):
|
37 |
+
"""Generate with Song Description"""
|
38 |
+
|
39 |
+
gpt_description_prompt: str
|
40 |
+
make_instrumental: bool = False
|
41 |
+
mv: str = Field(
|
42 |
+
default='chirp-v3-0',
|
43 |
+
description="model version, default: chirp-v3-0",
|
44 |
+
examples=["chirp-v3-0"],
|
45 |
+
)
|
46 |
+
|
47 |
+
prompt: str = Field(
|
48 |
+
default="",
|
49 |
+
description="Placeholder, keep it as an empty string, do not modify it",
|
50 |
+
)
|
51 |
+
|
52 |
+
generation_type: str = "TEXT"
|
53 |
+
user_uploaded_images_b64: str = ""
|
utils.py
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import os
|
3 |
+
import time
|
4 |
+
|
5 |
+
import aiohttp
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
|
8 |
+
load_dotenv()
|
9 |
+
|
10 |
+
BASE_URL = "https://studio-api.suno.ai"
|
11 |
+
|
12 |
+
COMMON_HEADERS = {
|
13 |
+
"Content-Type": "text/plain;charset=UTF-8",
|
14 |
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
15 |
+
"Referer": "https://suno.com",
|
16 |
+
"Origin": "https://suno.com",
|
17 |
+
}
|
18 |
+
|
19 |
+
|
20 |
+
async def fetch(url, headers=None, data=None, method="POST"):
|
21 |
+
if headers is None:
|
22 |
+
headers = {}
|
23 |
+
headers.update(COMMON_HEADERS)
|
24 |
+
if data is not None:
|
25 |
+
data = json.dumps(data)
|
26 |
+
|
27 |
+
print(data, method, headers, url)
|
28 |
+
|
29 |
+
async with aiohttp.ClientSession() as session:
|
30 |
+
try:
|
31 |
+
async with session.request(
|
32 |
+
method=method, url=url, data=data, headers=headers
|
33 |
+
) as resp:
|
34 |
+
return await resp.json()
|
35 |
+
except Exception as e:
|
36 |
+
return f"An error occurred: {e}"
|
37 |
+
|
38 |
+
|
39 |
+
async def get_feed(ids, token):
|
40 |
+
headers = {"Authorization": f"Bearer {token}"}
|
41 |
+
api_url = f"{BASE_URL}/api/feed/?ids={ids}"
|
42 |
+
response = await fetch(api_url, headers, method="GET")
|
43 |
+
return response
|
44 |
+
|
45 |
+
|
46 |
+
async def generate_music(data, token):
|
47 |
+
headers = {"Authorization": f"Bearer {token}"}
|
48 |
+
api_url = f"{BASE_URL}/api/generate/v2/"
|
49 |
+
response = await fetch(api_url, headers, data)
|
50 |
+
return response
|
51 |
+
|
52 |
+
|
53 |
+
async def generate_lyrics(prompt, token):
|
54 |
+
headers = {"Authorization": f"Bearer {token}"}
|
55 |
+
api_url = f"{BASE_URL}/api/generate/lyrics/"
|
56 |
+
data = {"prompt": prompt}
|
57 |
+
return await fetch(api_url, headers, data)
|
58 |
+
|
59 |
+
|
60 |
+
async def get_lyrics(lid, token):
|
61 |
+
headers = {"Authorization": f"Bearer {token}"}
|
62 |
+
api_url = f"{BASE_URL}/api/generate/lyrics/{lid}"
|
63 |
+
return await fetch(api_url, headers, method="GET")
|
64 |
+
|
65 |
+
|
66 |
+
async def get_credits(token):
|
67 |
+
headers = {"Authorization": f"Bearer {token}"}
|
68 |
+
api_url = f"{BASE_URL}/api/billing/info/"
|
69 |
+
respose = await fetch(api_url, headers, method="GET")
|
70 |
+
return {
|
71 |
+
"credits_left": respose['total_credits_left'],
|
72 |
+
#"period": respose['period'],
|
73 |
+
"monthly_limit": respose['monthly_limit'],
|
74 |
+
"monthly_usage": respose['monthly_usage']
|
75 |
+
}
|