├── __init__.py
├── requirements.txt
├── images
├── 创作歌词.jpg
└── 创作音乐.jpg
├── config.json.template
├── README.md
└── nicesuno.py
/__init__.py:
--------------------------------------------------------------------------------
1 | from .nicesuno import *
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | pathvalidate
--------------------------------------------------------------------------------
/images/创作歌词.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangxyd/nicesuno/HEAD/images/创作歌词.jpg
--------------------------------------------------------------------------------
/images/创作音乐.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangxyd/nicesuno/HEAD/images/创作音乐.jpg
--------------------------------------------------------------------------------
/config.json.template:
--------------------------------------------------------------------------------
1 | {
2 | "suno_api_bases": ["http://127.0.0.1:8000"],
3 | "music_create_prefixes": ["唱", "演唱"],
4 | "instrumental_create_prefixes": ["演奏"],
5 | "lyrics_create_prefixes": ["写歌", "作词"],
6 | "music_output_dir": "/tmp/nicesuno",
7 | "is_send_lyrics": true,
8 | "is_send_covers": true
9 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nicesuno
2 |
3 | 一款基于[Suno](https://suno.com/)和[Suno-API](https://github.com/SunoAI-API/Suno-API)创作音乐的chatgpt-on-wechat插件。
4 |
5 | + 使用方法:
6 | ```
7 | 1.创作声乐
8 | 用法:唱/演唱<提示词>
9 | 示例:唱明天会更好。
10 |
11 | 2.创作器乐
12 | 用法:演奏<提示词>
13 | 示例:演奏明天会更好。
14 |
15 | 3.创作歌词
16 | 用法:写歌/作词<提示词>
17 | 示例:写歌明天会更好。
18 |
19 | 4.自定义模式
20 | 用法:
21 | 唱/演唱/演奏
22 | 标题: <标题>
23 | 风格: <风格1> <风格2> ...
24 | <歌词>
25 | 备注:前三行必须为创作前缀、标题、风格,<标题><风格><歌词>三个值可以为空,但<风格><歌词>不可同时为空!
26 | ```
27 |
28 | ## 插件效果
29 |
30 | 1. 创作音乐
31 |
32 | 
33 |
34 | 2. Suno超过限额之后,仅创作歌词
35 |
36 | 
37 |
38 | ## 安装方法
39 |
40 | **1. 浏览器访问[Suno](https://suno.com/),获取当前账户的`session_id`和`Cookie`。**
41 |
42 | + 浏览器访问并登录Suno:https://suno.com/
43 | + 按F12键打开开发者工具,选择“网络”标签;
44 | + 稍等一分钟就会出现类似`tokens?_clerk_js_version=4.72.0-snapshot.vc141245`的请求,获取该Request URL中的`Session_id`以及`Cookie`;
45 | + `Session_id`获取示例:比如这里的Request URL为`https://clerk.suno.com/v1/client/sessions/sess_xeNbYcD4zOK89Vzwipl30x5gWq3/tokens?_clerk_js_version=4.72.0-snapshot.vc141245`,则`Session_id`是`sess_xeNbYcD4zOK89Vzwipl30x5gWq3`。
46 |
47 | **2. 部署SunoAI-API**
48 |
49 | + 详细的安装和配置步骤参考[Suno-API](https://github.com/SunoAI-API/Suno-API),这里只给出大致步骤:
50 | ```shell
51 | # 克隆代码
52 | git clone https://github.com/SunoAI-API/Suno-API.git
53 |
54 | # 配置Suno-API,首先拷贝模板文件.env.example到.env
55 | cd Suno-API
56 | cp .env.example .env
57 | # 然后编辑.env文件,将其中的SESSION_ID和COOKIE两个环境变量的值,分别替换为步骤1中获取的Session_id和Cookie
58 | BASE_URL=https://studio-api.suno.ai
59 | SESSION_ID=将我替换为步骤1中获取的Session_id
60 | COOKIE=将我替换为步骤1中获取的Cookie
61 |
62 | # 安装依赖
63 | pip3 install -r requirements.txt
64 |
65 | # 运行程序
66 | nohup uvicorn main:app &>> Suno-API.log &
67 |
68 | # 查看日志
69 | tail -f Suno-API.log
70 | ```
71 |
72 | **3. 安装Nicesuno插件**
73 |
74 | ```sh
75 | #installp https://github.com/wangxyd/nicesuno.git
76 | #scanp
77 | ```
78 | + Nicesuno的默认配置无需修改,即可使用Suno创作音乐。
79 |
80 | ## Nicesuno自定义配置
81 |
82 | + 如果需要自定义配置,可以按照如下方法修改:
83 | ```shell
84 | # 拷贝模板文件config.json.template到config.json
85 | cp config.json.template config.json
86 | # 编辑config.json
87 | {
88 | "suno_api_bases": ["http://127.0.0.1:8000"],
89 | "music_create_prefixes": ["唱", "演唱"],
90 | "instrumental_create_prefixes": ["演奏"],
91 | "lyrics_create_prefixes": ["写歌", "作词"],
92 | "music_output_dir": "/tmp/nicesuno",
93 | "is_send_lyrics": true,
94 | "is_send_covers": true
95 | }
96 | ```
97 |
98 | 以上配置项中:
99 |
100 | - `suno_api_bases`: Suno-API的监听地址和端口,注意该参数的值为一个字符串数组,后续用于实现自动切换Suno账号;
101 | - `music_create_prefixes`: 创作声乐的消息前缀,注意该参数的值为一个字符串数组;
102 | - `instrumental_create_prefixes`: 创作器乐的消息前缀,注意该参数的值为一个字符串数组;
103 | - `lyrics_create_prefixes`: 创作歌词的消息前缀,注意该参数的值为一个字符串数组;
104 | - `music_output_dir`: 创作的音乐的存储目录,默认为`/tmp/nicesuno`;
105 | - `is_send_lyrics`: 是否获取并发送歌词,默认为`true`;
106 | - `is_send_covers`: 是否下载并发送封面,默认为`true`。
107 |
108 | 有更好的想法或建议,欢迎积极提出哦~~~
--------------------------------------------------------------------------------
/nicesuno.py:
--------------------------------------------------------------------------------
1 | # encoding:utf-8
2 | import os
3 | import re
4 | import json
5 | import time
6 | import requests
7 | import threading
8 | from typing import List
9 | from pathvalidate import sanitize_filename
10 |
11 | import plugins
12 | from bridge.context import ContextType
13 | from bridge.reply import Reply, ReplyType
14 | from common.log import logger
15 | from plugins import *
16 |
17 | @plugins.register(
18 | name="Nicesuno",
19 | desire_priority=90,
20 | hidden=False,
21 | desc="一款基于Suno和Suno-API创作音乐的插件。",
22 | version="1.3",
23 | author="空心菜",
24 | )
25 | class Nicesuno(Plugin):
26 | def __init__(self):
27 | super().__init__()
28 | try:
29 | # 加载配置
30 | conf = super().load_config()
31 | # 配置不存在则使用默认配置
32 | if not conf:
33 | logger.debug("[Nicesuno] config.json not found, config.json.template used.")
34 | curdir = os.path.dirname(__file__)
35 | config_path = os.path.join(curdir, "config.json.template")
36 | if os.path.exists(config_path):
37 | with open(config_path, "r", encoding="utf-8") as f:
38 | conf = json.load(f)
39 | self.suno_api_bases = conf.get("suno_api_bases", [])
40 | self.music_create_prefixes = conf.get("music_create_prefixes", [])
41 | self.instrumental_create_prefixes = conf.get("instrumental_create_prefixes", [])
42 | self.lyrics_create_prefixes = conf.get("lyrics_create_prefixes", [])
43 | self.music_output_dir = conf.get("music_output_dir", "/tmp")
44 | self.is_send_lyrics = conf.get("is_send_lyrics", True)
45 | self.is_send_covers = conf.get("is_send_covers", True)
46 | if not os.path.exists(self.music_output_dir):
47 | logger.info(f"[Nicesuno] music_output_dir={self.music_output_dir} not exists, create it.")
48 | os.makedirs(self.music_output_dir)
49 | if self.suno_api_bases and isinstance(self.suno_api_bases, List) \
50 | and self.music_create_prefixes and isinstance(self.music_create_prefixes, List):
51 | self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
52 | logger.info("[Nicesuno] inited")
53 | else:
54 | logger.warn("[Nicesuno] init failed because suno_api_bases or music_create_prefixes is incorrect.")
55 | # 待实现:部署多套Suno-API,实现限额后自动切换Suno账号
56 | self.suno_api_base = self.suno_api_bases[0]
57 | except Exception as e:
58 | logger.error(f"[Nicesuno] init failed, ignored.")
59 | raise e
60 |
61 | def on_handle_context(self, e_context: EventContext):
62 | try:
63 | # 判断是否是TEXT类型消息
64 | context = e_context["context"]
65 | if context.type != ContextType.TEXT:
66 | return
67 | content = context.content
68 | logger.debug(f"[Nicesuno] on_handle_context. content={content}")
69 |
70 | # 判断是否包含创作的前缀
71 | make_instrumental, make_lyrics = False, False
72 | music_create_prefix = self._check_prefix(content, self.music_create_prefixes)
73 | instrumental_create_prefix = self._check_prefix(content, self.instrumental_create_prefixes)
74 | lyrics_create_prefix = self._check_prefix(content, self.lyrics_create_prefixes)
75 | if music_create_prefix:
76 | suno_prompt = content[len(music_create_prefix):].strip()
77 | elif instrumental_create_prefix:
78 | make_instrumental = True
79 | suno_prompt = content[len(instrumental_create_prefix):].strip()
80 | elif lyrics_create_prefix:
81 | make_lyrics = True
82 | suno_prompt = content[len(lyrics_create_prefix):].strip()
83 | else:
84 | logger.debug(f"[Nicesuno] content starts without any suno prefixes, ignored.")
85 | return
86 |
87 | # 判断是否包含创作的提示词
88 | if not suno_prompt:
89 | logger.info("[Nicesuno] content starts without any suno prompts, ignored.")
90 | return
91 |
92 | # 开始创作
93 | if make_lyrics:
94 | logger.info(f"[Nicesuno] start generating lyrics, suno_prompt={suno_prompt}.")
95 | self._create_lyrics(e_context, suno_prompt)
96 | else:
97 | logger.info(
98 | f"[Nicesuno] start generating {'instrumental' if make_instrumental else 'vocal'} music, suno_prompt={suno_prompt}.")
99 | self._create_music(e_context, suno_prompt, make_instrumental)
100 | except Exception as e:
101 | logger.warning(f"[Nicesuno] failed to generate music, error={e}")
102 | reply = Reply(ReplyType.TEXT, "抱歉!创作失败了,请稍后再试🥺")
103 | e_context["reply"] = reply
104 | e_context.action = EventAction.BREAK_PASS
105 |
106 | # 创作音乐
107 | def _create_music(self, e_context, suno_prompt, make_instrumental=False):
108 | custom_mode = False
109 | # 自定义模式
110 | if '标题' in suno_prompt and '风格' in suno_prompt:
111 | regex_prompt = r' *标题[::]?(?P
[\S ]*)\n+ *风格[::]?(?P[\S ]*)(\n+(?P.*))?'
112 | r = re.fullmatch(regex_prompt, suno_prompt, re.DOTALL)
113 | title = r.group('title').strip() if r and r.group('title') else None
114 | tags = r.group('tags').strip() if r and r.group('tags') else None
115 | lyrics = r.group('lyrics').strip() if r and r.group('lyrics') else None
116 | if r and (tags or lyrics):
117 | custom_mode = True
118 | logger.info(f"[Nicesuno] generating {'instrumental' if make_instrumental else 'vocal'} music in custom mode, title={title}, tags={tags}, lyrics={lyrics}")
119 | data = self._suno_generate_music_custom_mode(title, tags, lyrics, make_instrumental)
120 | else:
121 | logger.warning(f"[Nicesuno] generating {'instrumental' if make_instrumental else 'vocal'} music in custom mode failed because of wrong format, suno_prompt={suno_prompt}")
122 | reply = Reply(ReplyType.TEXT, self.get_help_text())
123 | e_context["reply"] = reply
124 | e_context.action = EventAction.BREAK_PASS
125 | return
126 | # 描述模式
127 | else:
128 | logger.info(f"[Nicesuno] generating {'instrumental' if make_instrumental else 'vocal'} music with description, description={suno_prompt}")
129 | data = self._suno_generate_music_with_description(suno_prompt, make_instrumental)
130 |
131 | channel = e_context["channel"]
132 | context = e_context["context"]
133 | to_user_nickname = context["msg"].to_user_nickname
134 | if not data:
135 | logger.warning(f"response data of _suno_generate_music is empty.")
136 | reply = Reply(ReplyType.TEXT, f"因为神秘原因,创作失败了😂请稍后再试...")
137 | # 如果Suno超过限额
138 | elif data.get('detail') == 'Insufficient credits.' and custom_mode:
139 | logger.warning(f"[Nicesuno] insufficient credits in custom mode.")
140 | reply = Reply(ReplyType.TEXT, f"Suno老师说一天只能创作5次😂今天确实唱够了,明天11点之后再来好不好😘")
141 | elif data.get('detail') == 'Insufficient credits.':
142 | logger.warning(f"[Nicesuno] insufficient credits with description, changed to generating lyrics...")
143 | reply = Reply(ReplyType.TEXT, f"Suno老师说一天只能创作5次😂今天确实唱够了,{to_user_nickname}来为你写歌好不好😘")
144 | self._create_lyrics(e_context, suno_prompt)
145 | # 如果Suno-API的Token失效
146 | elif data.get('detail'):
147 | logger.warning(f"[Nicesuno] error occurred, response data={data}")
148 | if data.get('detail') == 'Unauthorized':
149 | reply = Reply(ReplyType.TEXT, f"因为长期翘课,被Suno老师劝退了😂请重新找Suno老师申请入学...")
150 | elif data.get('detail') == 'Topic too long.':
151 | reply = Reply(ReplyType.TEXT, f"因为废话太多,被Suno老师打回了😂请重新提交创作申请...")
152 | elif data.get('detail') == 'Too many running jobs.':
153 | reply = Reply(ReplyType.TEXT, f"Suno老师说工作太忙😂请稍等片刻再创作...")
154 | else:
155 | reply = Reply(ReplyType.TEXT, f"因为{data.get('detail')},创作失败了😂请稍后再试...")
156 | elif not data.get('clips'):
157 | logger.warning(f"[Nicesuno] no clips in response data, response data={data}")
158 | reply = Reply(ReplyType.TEXT, f"因为神秘原因,创作失败了😂请稍后再试...")
159 | # 获取和发送音乐
160 | else:
161 | aids = [clip['id'] for clip in data['clips']]
162 | logger.debug(f"[Nicesuno] start to handle music, aids={aids}, data={data}")
163 | threading.Thread(target=self._handle_music, args=(channel, context, aids)).start()
164 | reply = Reply(ReplyType.TEXT, f"{to_user_nickname}正在为您创作音乐,请稍等☕")
165 | e_context["reply"] = reply
166 | e_context.action = EventAction.BREAK_PASS
167 |
168 | # 创作歌词
169 | def _create_lyrics(self, e_context, suno_prompt):
170 | data = self._suno_generate_lyrics(suno_prompt)
171 | channel = e_context["channel"]
172 | context = e_context["context"]
173 | if not data:
174 | error = f"response data of _suno_generate_lyrics is empty."
175 | raise Exception(error)
176 | # 获取和发送歌词
177 | lid = data['id']
178 | logger.debug(f"[Nicesuno] start to handle lyrics, lid={lid}, data={data}")
179 | threading.Thread(target=self._handle_lyric, args=(channel, context, lid, suno_prompt)).start()
180 | e_context.action = EventAction.BREAK_PASS
181 |
182 | # 下载和发送音乐
183 | def _handle_music(self, channel, context, aids: List):
184 | # 用户信息
185 | actual_user_nickname = context["msg"].actual_user_nickname or context["msg"].other_user_nickname
186 | to_user_nickname = context["msg"].to_user_nickname
187 | # 获取歌词和音乐
188 | initial_delay_seconds = 15
189 | last_lyrics = ""
190 | for aid in aids:
191 | # 获取音乐信息
192 | start_time = time.time()
193 | while True:
194 | if initial_delay_seconds:
195 | time.sleep(initial_delay_seconds)
196 | initial_delay_seconds = 0
197 | data = self._suno_get_music(aid)
198 | if not data:
199 | raise Exception("[Nicesuno] 获取音乐信息失败!")
200 | elif data["audio_url"]:
201 | break
202 | elif time.time() - start_time > 180:
203 | raise TimeoutError("[Nicesuno] 获取音乐信息超时!")
204 | time.sleep(5)
205 | # 解析音乐信息
206 | title, metadata, audio_url = data["title"], data["metadata"], data["audio_url"]
207 | lyrics, tags, description_prompt = metadata["prompt"], metadata["tags"], metadata['gpt_description_prompt']
208 | description_prompt = description_prompt if description_prompt else "自定义模式不展示"
209 | # 发送歌词
210 | if not self.is_send_lyrics:
211 | logger.debug(f"[Nicesuno] 发送歌词开关关闭,不发送歌词!")
212 | elif lyrics == last_lyrics:
213 | logger.debug("[Nicesuno] 歌词和上次相同,不再重复发送歌词!")
214 | else:
215 | reply_text = f"🎻{title}🎻\n\n{lyrics}\n\n🎹风格: {tags}\n👶发起人:{actual_user_nickname}\n🍀制作人:Suno\n🎤提示词: {description_prompt}"
216 | logger.debug(f"[Nicesuno] 发送歌词,reply_text={reply_text}")
217 | last_lyrics = lyrics
218 | reply = Reply(ReplyType.TEXT, reply_text)
219 | channel.send(reply, context)
220 | # 下载音乐
221 | filename = f"{int(time.time())}-{sanitize_filename(title).replace(' ', '')[:20]}"
222 | audio_path = os.path.join(self.music_output_dir, f"{filename}.mp3")
223 | logger.debug(f"[Nicesuno] 下载音乐,audio_url={audio_url}")
224 | self._download_file(audio_url, audio_path)
225 | # 发送音乐
226 | logger.debug(f"[Nicesuno] 发送音乐,audio_path={audio_path}")
227 | reply = Reply(ReplyType.FILE, audio_path)
228 | channel.send(reply, context)
229 | # 发送封面
230 | if not self.is_send_covers:
231 | logger.debug(f"[Nicesuno] 发送封面开关关闭,不发送封面!")
232 | else:
233 | # 获取封面信息
234 | start_time = time.time()
235 | while True:
236 | data = self._suno_get_music(aid)
237 | if not data:
238 | #raise Exception("[Nicesuno] 获取封面信息失败!")
239 | logger.warning("[Nicesuno] 获取封面信息失败!")
240 | break
241 | elif data["image_url"]:
242 | break
243 | elif time.time() - start_time > 60:
244 | #raise TimeoutError("[Nicesuno] 获取封面信息超时!")
245 | logger.warning("[Nicesuno] 获取封面信息超时!")
246 | break
247 | time.sleep(5)
248 | if data and data["image_url"]:
249 | image_url = data["image_url"]
250 | logger.debug(f"[Nicesuno] 发送封面,image_url={image_url}")
251 | reply = Reply(ReplyType.IMAGE_URL, image_url)
252 | channel.send(reply, context)
253 | else:
254 | logger.warning(f"[Nicesuno] 获取封面信息失败,放弃发送封面!")
255 | # 获取视频地址
256 | video_urls = []
257 | for aid in aids:
258 | # 获取视频地址
259 | start_time = time.time()
260 | while True:
261 | data = self._suno_get_music(aid)
262 | if not data:
263 | #raise Exception("[Nicesuno] 获取视频地址失败!")
264 | logger.warning("[Nicesuno] 获取视频地址失败!")
265 | video_urls.append("获取失败!")
266 | break
267 | elif data["video_url"]:
268 | video_urls.append(data["video_url"])
269 | break
270 | elif time.time() - start_time > 180:
271 | #raise TimeoutError("[Nicesuno] 获取视频地址超时!")
272 | logger.warning("[Nicesuno] 获取视频地址超时!")
273 | video_urls.append("获取超时!")
274 | time.sleep(10)
275 | # 查收提醒
276 | video_text = '\n'.join(f'视频{idx+1}: {url}' for idx, url in zip(range(len(video_urls)), video_urls))
277 | reply_text = f"{to_user_nickname}已经为您创作了音乐,请查收!以下是音乐视频:\n{video_text}"
278 | if context.get("isgroup", False):
279 | reply_text = f"@{actual_user_nickname}\n" + reply_text
280 | logger.debug(f"[Nicesuno] 发送查收提醒,reply_text={reply_text}")
281 | reply = Reply(ReplyType.TEXT, reply_text)
282 | channel.send(reply, context)
283 |
284 | # 获取和发送歌词
285 | def _handle_lyric(self, channel, context, lid, description_prompt=""):
286 | # 用户信息
287 | actual_user_nickname = context["msg"].actual_user_nickname or context["msg"].other_user_nickname
288 | # 获取歌词信息
289 | start_time = time.time()
290 | while True:
291 | data = self._suno_get_lyrics(lid)
292 | if not data:
293 | raise Exception("[Nicesuno] 获取歌词信息失败!")
294 | elif data["status"] == 'complete':
295 | break
296 | elif time.time() - start_time > 120:
297 | raise TimeoutError("[Nicesuno] 获取歌词信息超时!")
298 | time.sleep(5)
299 | # 发送歌词
300 | title, lyrics = data["title"], data["text"]
301 | reply_text = f"🎻{title}🎻\n\n{lyrics}\n\n👶发起人:{actual_user_nickname}\n🍀制作人:Suno\n🎤提示词: {description_prompt}"
302 | logger.debug(f"[Nicesuno] 发送歌词,reply_text={reply_text}")
303 | reply = Reply(ReplyType.TEXT, reply_text)
304 | channel.send(reply, context)
305 |
306 | # 创作音乐
307 | def _suno_generate_music_with_description(self, description, make_instrumental=False, retry_count=0):
308 | payload = {
309 | "gpt_description_prompt": description,
310 | "make_instrumental": make_instrumental,
311 | "mv": "chirp-v3-0",
312 | }
313 | while retry_count >= 0:
314 | try:
315 | response = requests.post(f"{self.suno_api_base}/generate/description-mode", data=json.dumps(payload), timeout=(5, 30))
316 | if response.status_code != 200:
317 | raise Exception(f"status_code is not ok, status_code={response.status_code}")
318 | logger.debug(f"[Nicesuno] _suno_generate_music_with_description, response={response.text}")
319 | return response.json()
320 | except Exception as e:
321 | logger.error(f"[Nicesuno] _suno_generate_music_with_description failed, description={description}, error={e}")
322 | retry_count -= 1
323 | time.sleep(5)
324 |
325 | # 创作音乐
326 | def _suno_generate_music_custom_mode(self, title=None, tags=None, lyrics=None, make_instrumental=False, retry_count=0):
327 | payload = {
328 | "title": title,
329 | "tags": tags,
330 | "prompt": lyrics,
331 | "make_instrumental": make_instrumental,
332 | "mv": "chirp-v3-0",
333 | "continue_clip_id": None,
334 | "continue_at": None,
335 | }
336 | while retry_count >= 0:
337 | try:
338 | response = requests.post(f"{self.suno_api_base}/generate", data=json.dumps(payload), timeout=(5, 30))
339 | if response.status_code != 200:
340 | raise Exception(f"status_code is not ok, status_code={response.status_code}")
341 | logger.debug(f"[Nicesuno] _suno_generate_music_custom_mode, response={response.text}")
342 | return response.json()
343 | except Exception as e:
344 | logger.error(f"[Nicesuno] _suno_generate_music_custom_mode failed, title={title}, tags={tags}, lyrics={lyrics}, error={e}")
345 | retry_count -= 1
346 | time.sleep(5)
347 |
348 | # 获取音乐信息
349 | def _suno_get_music(self, aid, retry_count=3):
350 | while retry_count >= 0:
351 | try:
352 | response = requests.get(f"{self.suno_api_base}/feed/{aid}", timeout=(5, 30))
353 | if response.status_code != 200:
354 | raise Exception(f"status_code is not ok, status_code={response.status_code}")
355 | logger.debug(f"[Nicesuno] _suno_get_music, response={response.text}")
356 | return response.json()[0]
357 | except Exception as e:
358 | logger.error(f"[Nicesuno] _suno_get_music failed, aid={aid}, error={e}")
359 | retry_count -= 1
360 | time.sleep(5)
361 |
362 | # 创作歌词
363 | def _suno_generate_lyrics(self, suno_lyric_prompt, retry_count=3):
364 | payload = {
365 | "prompt": suno_lyric_prompt
366 | }
367 | while retry_count >= 0:
368 | try:
369 | response = requests.post(f"{self.suno_api_base}/generate/lyrics/", data=json.dumps(payload), timeout=(5, 30))
370 | if response.status_code != 200:
371 | raise Exception(f"status_code is not ok, status_code={response.status_code}")
372 | logger.debug(f"[Nicesuno] _suno_generate_lyrics, response={response.text}")
373 | return response.json()
374 | except Exception as e:
375 | logger.error(f"[Nicesuno] _suno_generate_lyrics failed, suno_lyric_prompt={suno_lyric_prompt}, error={e}")
376 | retry_count -= 1
377 | time.sleep(5)
378 |
379 | # 获取歌词信息
380 | def _suno_get_lyrics(self, lid, retry_count=3):
381 | while retry_count >= 0:
382 | try:
383 | response = requests.get(f"{self.suno_api_base}/lyrics/{lid}", timeout=(5, 30))
384 | if response.status_code != 200:
385 | raise Exception(f"status_code is not ok, status_code={response.status_code}")
386 | logger.debug(f"[Nicesuno] _suno_get_lyrics, response={response.text}")
387 | return response.json()
388 | except Exception as e:
389 | logger.error(f"[Nicesuno] _suno_get_lyrics failed, lid={lid}, error={e}")
390 | retry_count -= 1
391 | time.sleep(5)
392 |
393 | # 下载文件
394 | def _download_file(self, file_url, file_path, retry_count=3):
395 | while retry_count >= 0:
396 | try:
397 | response = requests.get(file_url, allow_redirects=True, stream=True)
398 | if response.status_code != 200:
399 | raise Exception(f"[Nicesuno] 文件下载失败,file_url={file_url}, status_code={response.status_code}")
400 | with open(file_path, "wb") as f:
401 | for chunk in response.iter_content(chunk_size=1024):
402 | if chunk:
403 | f.write(chunk)
404 | except Exception as e:
405 | logger.error(f"[Nicesuno] 文件下载失败,file_url={file_url}, error={e}")
406 | retry_count -= 1
407 | time.sleep(5)
408 | else:
409 | break
410 | # 检查是否包含创作音乐的前缀
411 | def _check_prefix(self, content, prefix_list):
412 | if not prefix_list:
413 | return None
414 | for prefix in prefix_list:
415 | if content.startswith(prefix):
416 | return prefix
417 | return None
418 |
419 | # 帮助文档
420 | def get_help_text(self, **kwargs):
421 | return "使用Suno创作音乐。\n1.创作声乐\n用法:唱/演唱<提示词>\n示例:唱明天会更好。\n\n2.创作器乐\n用法:演奏<提示词>\n示例:演奏明天会更好。\n\n3.创作歌词\n用法:写歌/作词<提示词>\n示例:写歌明天会更好。\n\n4.自定义模式\n用法:\n唱/演唱/演奏\n标题: <标题>\n风格: <风格1> <风格2> ...\n<歌词>\n备注:前三行必须为创作前缀、标题、风格,<标题><风格><歌词>三个值可以为空,但<风格><歌词>不可同时为空!"
422 |
--------------------------------------------------------------------------------