Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -1623,10 +1623,14 @@ class ChatProviderTemplate(TypedDict):
"type": "gsvi_tts_api",
"provider": "gpt_sovits_inference",
"provider_type": "text_to_speech",
"api_base": "http://127.0.0.1:5000",
"character": "",
"emotion": "default",
"enable": False,
Comment thread
Rain-0x01-39 marked this conversation as resolved.
"api_key": "",
"api_base": "http://127.0.0.1:8000",
"version": "v4",
"character": "",
"prompt_text_lang": "中文",
"emotion": "默认",
"text_lang": "中文",
"timeout": 20,
},
"FishAudio TTS(API)": {
Expand Down
57 changes: 37 additions & 20 deletions astrbot/core/provider/sources/gsvi_tts_source.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import urllib.parse
import uuid
from pathlib import Path

import aiohttp

Expand All @@ -23,37 +22,55 @@ def __init__(
provider_settings: dict,
) -> None:
super().__init__(provider_config, provider_settings)
self.api_base = provider_config.get("api_base", "http://127.0.0.1:5000")
self.api_key = provider_config.get("api_key", "")
self.api_base = provider_config.get("api_base", "http://127.0.0.1:8000")
self.api_base = self.api_base.removesuffix("/")
self.version = provider_config.get("version", "v4")
self.character = provider_config.get("character")
self.emotion = provider_config.get("emotion")
self.prompt_text_lang = provider_config.get("prompt_text_lang", "中文")
self.emotion = provider_config.get("emotion", "默认")
self.text_lang = provider_config.get("text_lang", "中文")

async def get_audio(self, text: str) -> str:
temp_dir = get_astrbot_temp_path()
path = os.path.join(temp_dir, f"gsvi_tts_{uuid.uuid4()}.wav")
params = {"text": text}
path = Path(temp_dir) / f"gsvi_tts_{uuid.uuid4()}.wav"
url = f"{self.api_base}/infer_single"

if self.character:
params["character"] = self.character
if self.emotion:
params["emotion"] = self.emotion
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"

query_parts = []
for key, value in params.items():
encoded_value = urllib.parse.quote(str(value))
query_parts.append(f"{key}={encoded_value}")

url = f"{self.api_base}/tts?{'&'.join(query_parts)}"
data = {
"dl_url": self.api_base,
"version": self.version,
"model_name": self.character,
"prompt_text_lang": self.prompt_text_lang,
"emotion": self.emotion,
"text": text,
"text_lang": self.text_lang,
}

async with aiohttp.ClientSession() as session:
Comment thread
Rain-0x01-39 marked this conversation as resolved.
async with session.get(url) as response:
async with session.post(url, json=data, headers=headers) as response:
if response.status == 200:
with open(path, "wb") as f:
f.write(await response.read())
resp_json = await response.json()
msg = resp_json.get("msg")
audio_url = resp_json.get("audio_url")
if not msg or msg != "合成成功":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

魔法字符串 "合成成功" 用于判断 API 调用是否成功。建议将其定义为模块级别的常量,例如在文件顶部定义 _SUCCESS_MSG = "合成成功",然后在代码中引用该常量。这会提高代码的可读性和可维护性。

raise Exception(f"GSVI TTS API 合成失败: {msg}")
async with session.get(audio_url) as audio_response:
if audio_response.status == 200:
with open(path, "wb") as f:
f.write(await audio_response.read())
Comment on lines +63 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在异步函数中使用了同步的文件写入操作 open(...)f.write(...),这可能会阻塞事件循环。虽然在单线程 asyncio 事件循环中,同步代码块是原子执行的,不存在竞态条件,但为了保证事件循环的响应性,建议使用 aiofiles 库来进行异步文件操作。

请在文件顶部添加 import aiofiles,然后将此处的文件操作修改为异步方式。

Suggested change
with open(path, "wb") as f:
f.write(await audio_response.read())
async with aiofiles.open(path, "wb") as f:
await f.write(await audio_response.read())
References
  1. In a single-threaded asyncio event loop, synchronous functions (code blocks without 'await') are executed atomically and will not be interrupted by other coroutines. Therefore, they are safe from race conditions when modifying shared state within that block.

else:
error_text = await audio_response.text()
raise Exception(
f"GSVI TTS API 下载音频失败,状态码: {audio_response.status},错误: {error_text}",
)
else:
error_text = await response.text()
raise Exception(
f"GSVI TTS API 请求失败,状态码: {response.status},错误: {error_text}",
)

return path
return str(path)