├── __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 | ![创作音乐](./images/创作音乐.jpg) 33 | 34 | 2. Suno超过限额之后,仅创作歌词 35 | 36 | ![创作歌词](./images/创作歌词.jpg) 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<tags>[\S ]*)(\n+(?P<lyrics>.*))?' 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 | --------------------------------------------------------------------------------