-
-
Notifications
You must be signed in to change notification settings - Fork 672
feat: 新增GPT_SoVIS(本地加载)的TTS服务商 #1821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
变更 | 详情 | 文件 |
---|---|---|
集成了自托管的 GPT_SoVITS TTS provider 适配器 |
|
astrbot/core/provider/sources/gsv_selfhosted_source.py astrbot/core/provider/manager.py |
扩展了默认配置模式以支持本地 TTS 和概率回复标志 |
|
astrbot/core/config/default.py |
提示和命令
与 Sourcery 交互
- 触发新的审查: 在 pull request 上评论
@sourcery-ai review
。 - 继续讨论: 直接回复 Sourcery 的审查评论。
- 从审查评论生成 GitHub issue: 通过回复审查评论,要求 Sourcery 从该评论创建一个 issue。 您也可以回复审查评论并使用
@sourcery-ai issue
从该评论创建一个 issue。 - 生成 pull request 标题: 在 pull request 标题中的任何位置写入
@sourcery-ai
以随时生成标题。 您也可以在 pull request 上评论@sourcery-ai title
以随时(重新)生成标题。 - 生成 pull request 摘要: 在 pull request 正文中的任何位置写入
@sourcery-ai summary
以随时在您想要的位置生成 PR 摘要。 您也可以在 pull request 上评论@sourcery-ai summary
以随时(重新)生成摘要。 - 生成审查者指南: 在 pull request 上评论
@sourcery-ai guide
以随时(重新)生成审查者指南。 - 解决所有 Sourcery 评论: 在 pull request 上评论
@sourcery-ai resolve
以解决所有 Sourcery 评论。 如果您已经解决了所有评论并且不想再看到它们,这将非常有用。 - 驳回所有 Sourcery 审查: 在 pull request 上评论
@sourcery-ai dismiss
以驳回所有现有的 Sourcery 审查。 如果您想从新的审查开始,这将特别有用 - 不要忘记评论@sourcery-ai review
以触发新的审查!
自定义您的体验
访问您的 dashboard 以:
- 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
- 更改审查语言。
- 添加、删除或编辑自定义审查说明。
- 调整其他审查设置。
获取帮助
- 联系我们的支持团队 提出问题或反馈。
- 访问我们的 documentation 获取详细指南和信息。
- 通过在 X/Twitter、LinkedIn 或 GitHub 上关注我们,与 Sourcery 团队保持联系。
Original review guide in English
Reviewer's Guide
This PR integrates a self-hosted GPT_SoVITS TTS service into the provider framework by adding a new ProviderGSVTTS adapter and updating the provider manager, while extending the default configuration schema to include the new TTS settings and converting reply flags to probabilistic floats.
Sequence Diagram for TTS Request via gsv_tts_selfhost
sequenceDiagram
participant C as Client/System
participant P_GSV as ProviderGSVTTS
participant GSV_API as "GPT-SoVITS API (Local)"
C->>P_GSV: get_audio(text)
activate P_GSV
P_GSV->>GSV_API: GET /tts (text, params)
activate GSV_API
GSV_API-->>P_GSV: audio_bytes
deactivate GSV_API
P_GSV->>P_GSV: Save audio_bytes to file
P_GSV-->>C: audio_file_path
deactivate P_GSV
Sequence Diagram for ProviderGSVTTS
Initialization and Model Setup
sequenceDiagram
participant System as System
participant P_GSV as ProviderGSVTTS
participant GSV_API as "GPT-SoVITS API (Local)"
System->>P_GSV: __init__(config, settings)
activate P_GSV
P_GSV->>P_GSV: Async task: _set_model_weights()
alt gpt_weights_path configured
P_GSV->>GSV_API: GET /set_gpt_weights?weights_path=...
activate GSV_API
GSV_API-->>P_GSV: HTTP Response
deactivate GSV_API
end
alt sovits_weights_path configured
P_GSV->>GSV_API: GET /set_sovits_weights?weights_path=...
activate GSV_API
GSV_API-->>P_GSV: HTTP Response
deactivate GSV_API
end
P_GSV-->>System: ProviderGSVTTS instance
deactivate P_GSV
ER Diagram for Configuration Updates in default.py
erDiagram
GlobalConfig {
float reply_with_mention "Changed: bool to float (0.0-1.0 probability)"
float reply_with_quote "Changed: bool to float (0.0-1.0 probability)"
map TTS_Providers "Map of TTS provider configurations"
}
TTS_Providers ||--|{ GSV_TTS_ProviderConfig : "New entry: gsv_tts_selfhost"
GSV_TTS_ProviderConfig {
string id "e.g., 'gsv_tts'"
bool enable "Enable/disable provider"
string type "'gsv_tts_selfhost'"
string provider_type "'text_to_speech'"
string api_base "Local GPT-SoVITS API base URL"
string gpt_weights_path "Path to GPT model (optional)"
string sovits_weights_path "Path to SoVITS model (optional)"
object gsv_default_parms "Default synthesis parameters"
}
GSV_TTS_ProviderConfig ||--|{ GSV_DefaultParameters : "gsv_default_parms"
GSV_DefaultParameters {
string gsv_ref_audio_path "Reference audio path (required)"
string gsv_prompt_text "Reference audio prompt text (required)"
string gsv_prompt_lang "Language of prompt text (e.g., 'zh')"
string gsv_text_lang "Language of text to synthesize (e.g., 'zh')"
int gsv_top_k "Top K sampling"
float gsv_top_p "Top P (nucleus) sampling"
float gsv_temperature "Sampling temperature"
string gsv_text_split_method "Text splitting method (e.g., 'cut3')"
int gsv_batch_size "Inference batch size"
float gsv_batch_threshold "Batch threshold"
bool gsv_split_bucket "Enable split bucket processing"
float gsv_speed_factor "Speech speed factor"
float gsv_fragment_interval "Fragment interval for streaming"
bool gsv_streaming_mode "Enable streaming mode output"
int gsv_seed "Random seed for reproducibility"
bool gsv_parallel_infer "Enable parallel inference"
float gsv_repetition_penalty "Repetition penalty factor"
string gsv_media_type "Output audio format (e.g., 'wav')"
string gsv_aux_ref_audio_paths "Auxiliary reference audio paths (optional)"
}
Class Diagram for the New ProviderGSVTTS
classDiagram
class TTSProvider {
<<Interface>>
+get_audio(text: str) str
}
class ProviderGSVTTS {
-api_base: str
-gpt_weights_path: str
-sovits_weights_path: str
-default_params: dict
-emotions: dict
+__init__(provider_config: dict, provider_settings: dict)
-_make_request(endpoint: str, params: dict) str | bytes
-_set_model_weights() None
+get_audio(text: str) str
}
TTSProvider <|-- ProviderGSVTTS
File-Level Changes
Change | Details | Files |
---|---|---|
Integrated self-hosted GPT_SoVITS TTS provider adapter |
|
astrbot/core/provider/sources/gsv_selfhosted_source.py astrbot/core/provider/manager.py |
Extended default configuration schema to support local TTS and probabilistic reply flags |
|
astrbot/core/config/default.py |
Tips and commands
Interacting with Sourcery
- Trigger a new review: Comment
@sourcery-ai review
on the pull request. - Continue discussions: Reply directly to Sourcery's review comments.
- Generate a GitHub issue from a review comment: Ask Sourcery to create an
issue from a review comment by replying to it. You can also reply to a
review comment with@sourcery-ai issue
to create an issue from it. - Generate a pull request title: Write
@sourcery-ai
anywhere in the pull
request title to generate a title at any time. You can also comment
@sourcery-ai title
on the pull request to (re-)generate the title at any time. - Generate a pull request summary: Write
@sourcery-ai summary
anywhere in
the pull request body to generate a PR summary at any time exactly where you
want it. You can also comment@sourcery-ai summary
on the pull request to
(re-)generate the summary at any time. - Generate reviewer's guide: Comment
@sourcery-ai guide
on the pull
request to (re-)generate the reviewer's guide at any time. - Resolve all Sourcery comments: Comment
@sourcery-ai resolve
on the
pull request to resolve all Sourcery comments. Useful if you've already
addressed all the comments and don't want to see them anymore. - Dismiss all Sourcery reviews: Comment
@sourcery-ai dismiss
on the pull
request to dismiss all existing Sourcery reviews. Especially useful if you
want to start fresh with a new review - don't forget to comment
@sourcery-ai review
to trigger a new review!
Customizing Your Experience
Access your dashboard to:
- Enable or disable review features such as the Sourcery-generated pull request
summary, the reviewer's guide, and others. - Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.
Getting Help
- Contact our support team for questions or feedback.
- Visit our documentation for detailed guides and information.
- Keep in touch with the Sourcery team by following us on X/Twitter, LinkedIn or GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
嘿 @Zhalslar - 我已经审查了你的更改 - 这里有一些反馈:
- 在每个请求上创建一个新的 aiohttp ClientSession 可能会很昂贵 - 考虑为提供程序重用单个会话实例。
- 将
_set_model_weights
作为未等待的后台任务启动可能会隐藏初始化失败; 考虑在提供程序设置期间等待它或将错误显示给调用者。 - 在
get_audio
中,临时文件名的前缀是gsvi_tts_
,但提供程序是gsv_tts
; 更新前缀可以防止混淆。
AI 代理的提示
请解决此代码审查中的评论:
## 总体评论
- 在每个请求上创建一个新的 aiohttp ClientSession 可能会很昂贵 - 考虑为提供程序重用单个会话实例。
- 将 `_set_model_weights` 作为未等待的后台任务启动可能会隐藏初始化失败; 考虑在提供程序设置期间等待它或将错误显示给调用者。
- 在 `get_audio` 中,临时文件名的前缀是 `gsvi_tts_`,但提供程序是 `gsv_tts`; 更新前缀可以防止混淆。
## 单独评论
### 评论 1
<location> `astrbot/core/provider/sources/gsv_selfhosted_source.py:38` </location>
<code_context>
+
+ # 默认参数
+ raw_params = provider_config.get("gsv_default_parms", {})
+ self.default_params: dict = {
+ key.removeprefix("gsv_"): str(value).lower()
+ for key, value in raw_params.items()
+ }
+
</code_context>
<issue_to_address>
避免将所有参数转换为小写字符串
将所有值转换为小写字符串会将整数、浮点数和布尔值更改为字符串,这可能会导致 API 中的类型问题。 尽可能保留原始类型。
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
self.default_params: dict = {
key.removeprefix("gsv_"): str(value).lower()
for key, value in raw_params.items()
}
=======
self.default_params: dict = {
key.removeprefix("gsv_"): value.lower() if isinstance(value, str) else value
for key, value in raw_params.items()
}
>>>>>>> REPLACE
</suggested_fix>
### 评论 2
<location> `astrbot/core/provider/sources/gsv_selfhosted_source.py:99` </location>
<code_context>
+
+ logger.debug(f"正在调用GSV语音合成接口,参数:{params}")
+
+ result = await self._make_request(endpoint, params)
+ if isinstance(result, bytes):
+ with open(path, "wb") as f:
</code_context>
<issue_to_address>
考虑使用 POST 而不是 GET 进行 TTS 请求
使用带有 JSON 主体的 POST 可以避免 URL 长度问题,并更好地支持大型文本输入。
建议的实施方式:
```python
result = await self._make_request(endpoint, params, method="POST", json_body=True)
```
您还需要更新 `_make_request` 方法:
- 接受一个 `method` 参数(默认为 "GET" 以实现向后兼容)。
- 接受一个 `json_body` 参数(默认为 False)。
- 如果 `method` 是 "POST" 且 `json_body` 为 True,则使用 `json=params` 发送请求,而不是 `params=params`。
示例签名:
```python
async def _make_request(self, endpoint, params, method="GET", json_body=False):
# implementation...
```
并且在实现中,使用:
```python
if method == "POST" and json_body:
async with session.post(endpoint, json=params) as resp:
...
else:
async with session.get(endpoint, params=params) as resp:
...
```
根据需要进行调整以适应您的代码库。
</issue_to_address>
### 评论 3
<location> `astrbot/core/provider/sources/gsv_selfhosted_source.py:66` </location>
<code_context>
+ if self.gpt_weights_path:
+ gpt_endpoint = f"{self.api_base}/set_gpt_weights"
+ gpt_params = {"weights_path": self.gpt_weights_path}
+ if await self._make_request(endpoint=gpt_endpoint, params=gpt_params):
+ logger.info(f"成功设置 GPT 模型路径:{self.gpt_weights_path}")
+ else:
</code_context>
<issue_to_address>
不要依赖响应内容的真值来检查成功
依赖响应的真值可能会导致误报。 而是,检查 HTTP 状态代码或让 `_make_request` 在非 200 响应时引发异常。
</issue_to_address>
帮助我更有用! 请点击每个评论上的 👍 或 👎,我将使用反馈来改进您的评论。
Original comment in English
Hey @Zhalslar - I've reviewed your changes - here's some feedback:
- Creating a new aiohttp ClientSession on every request can be expensive—consider reusing a single session instance for the provider.
- Launching
_set_model_weights
as an unawaited background task may hide initialization failures; consider awaiting it during provider setup or surface errors to the caller. - In
get_audio
, the temp filename is prefixed withgsvi_tts_
but the provider isgsv_tts
; updating the prefix would prevent confusion.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Creating a new aiohttp ClientSession on every request can be expensive—consider reusing a single session instance for the provider.
- Launching `_set_model_weights` as an unawaited background task may hide initialization failures; consider awaiting it during provider setup or surface errors to the caller.
- In `get_audio`, the temp filename is prefixed with `gsvi_tts_` but the provider is `gsv_tts`; updating the prefix would prevent confusion.
## Individual Comments
### Comment 1
<location> `astrbot/core/provider/sources/gsv_selfhosted_source.py:38` </location>
<code_context>
+
+ # 默认参数
+ raw_params = provider_config.get("gsv_default_parms", {})
+ self.default_params: dict = {
+ key.removeprefix("gsv_"): str(value).lower()
+ for key, value in raw_params.items()
+ }
+
</code_context>
<issue_to_address>
Avoid converting all params to lowercase strings
Converting all values to lowercase strings changes ints, floats, and bools to strings, which can cause type issues in the API. Preserve original types where possible.
</issue_to_address>
<suggested_fix>
<<<<<<< SEARCH
self.default_params: dict = {
key.removeprefix("gsv_"): str(value).lower()
for key, value in raw_params.items()
}
=======
self.default_params: dict = {
key.removeprefix("gsv_"): value.lower() if isinstance(value, str) else value
for key, value in raw_params.items()
}
>>>>>>> REPLACE
</suggested_fix>
### Comment 2
<location> `astrbot/core/provider/sources/gsv_selfhosted_source.py:99` </location>
<code_context>
+
+ logger.debug(f"正在调用GSV语音合成接口,参数:{params}")
+
+ result = await self._make_request(endpoint, params)
+ if isinstance(result, bytes):
+ with open(path, "wb") as f:
</code_context>
<issue_to_address>
Consider using POST instead of GET for TTS requests
Using POST with a JSON body avoids URL length issues and better supports large text inputs.
Suggested implementation:
```python
result = await self._make_request(endpoint, params, method="POST", json_body=True)
```
You will also need to update the `_make_request` method to:
- Accept a `method` parameter (defaulting to "GET" for backward compatibility).
- Accept a `json_body` parameter (defaulting to False).
- If `method` is "POST" and `json_body` is True, send the request with `json=params` instead of `params=params`.
Example signature:
```python
async def _make_request(self, endpoint, params, method="GET", json_body=False):
# implementation...
```
And in the implementation, use:
```python
if method == "POST" and json_body:
async with session.post(endpoint, json=params) as resp:
...
else:
async with session.get(endpoint, params=params) as resp:
...
```
Adjust as needed to fit your codebase.
</issue_to_address>
### Comment 3
<location> `astrbot/core/provider/sources/gsv_selfhosted_source.py:66` </location>
<code_context>
+ if self.gpt_weights_path:
+ gpt_endpoint = f"{self.api_base}/set_gpt_weights"
+ gpt_params = {"weights_path": self.gpt_weights_path}
+ if await self._make_request(endpoint=gpt_endpoint, params=gpt_params):
+ logger.info(f"成功设置 GPT 模型路径:{self.gpt_weights_path}")
+ else:
</code_context>
<issue_to_address>
Don’t rely on truthiness of response content for success check
Relying on the response's truthiness may lead to false positives. Instead, check the HTTP status code or have `_make_request` raise on non-200 responses.
</issue_to_address>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
self.default_params: dict = { | ||
key.removeprefix("gsv_"): str(value).lower() | ||
for key, value in raw_params.items() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: 避免将所有参数转换为小写字符串
将所有值转换为小写字符串会将整数、浮点数和布尔值更改为字符串,这可能会导致 API 中的类型问题。 尽可能保留原始类型。
self.default_params: dict = { | |
key.removeprefix("gsv_"): str(value).lower() | |
for key, value in raw_params.items() | |
} | |
self.default_params: dict = { | |
key.removeprefix("gsv_"): value.lower() if isinstance(value, str) else value | |
for key, value in raw_params.items() | |
} |
Original comment in English
suggestion: Avoid converting all params to lowercase strings
Converting all values to lowercase strings changes ints, floats, and bools to strings, which can cause type issues in the API. Preserve original types where possible.
self.default_params: dict = { | |
key.removeprefix("gsv_"): str(value).lower() | |
for key, value in raw_params.items() | |
} | |
self.default_params: dict = { | |
key.removeprefix("gsv_"): value.lower() if isinstance(value, str) else value | |
for key, value in raw_params.items() | |
} |
if self.gpt_weights_path: | ||
gpt_endpoint = f"{self.api_base}/set_gpt_weights" | ||
gpt_params = {"weights_path": self.gpt_weights_path} | ||
if await self._make_request(endpoint=gpt_endpoint, params=gpt_params): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): 不要依赖响应内容的真值来检查成功
依赖响应的真值可能会导致误报。 而是,检查 HTTP 状态代码或让 _make_request
在非 200 响应时引发异常。
Original comment in English
issue (bug_risk): Don’t rely on truthiness of response content for success check
Relying on the response's truthiness may lead to false positives. Instead, check the HTTP status code or have _make_request
raise on non-200 responses.
f.write(result) | ||
return path | ||
else: | ||
raise Exception(f"GSVI TTS API 请求失败: {result}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (code-quality): 引发一个特定的错误,而不是通用的 Exception
或 BaseException
(raise-specific-error
)
Explanation
如果一段代码引发一个特定的异常类型, 而不是通用的 [`BaseException`](https://docs.python.org/3/library/exceptions.html#BaseException) 或 [`Exception`](https://docs.python.org/3/library/exceptions.html#Exception), 调用代码可以:- 获取更多关于它是什么类型的错误的信息
- 为它定义特定的异常处理
这样,代码的调用者可以适当地处理错误。
您如何解决这个问题?
因此,与其让代码引发 Exception
或 BaseException
,例如
if incorrect_input(value):
raise Exception("The input is incorrect")
您可以让代码引发一个特定的错误,例如
if incorrect_input(value):
raise ValueError("The input is incorrect")
或者
class IncorrectInputError(Exception):
pass
if incorrect_input(value):
raise IncorrectInputError("The input is incorrect")
Original comment in English
issue (code-quality): Raise a specific error instead of the general Exception
or BaseException
(raise-specific-error
)
Explanation
If a piece of code raises a specific exception type rather than the generic [`BaseException`](https://docs.python.org/3/library/exceptions.html#BaseException) or [`Exception`](https://docs.python.org/3/library/exceptions.html#Exception), the calling code can:- get more information about what type of error it is
- define specific exception handling for it
This way, callers of the code can handle the error appropriately.
How can you solve this?
- Use one of the built-in exceptions of the standard library.
- Define your own error class that subclasses
Exception
.
So instead of having code raising Exception
or BaseException
like
if incorrect_input(value):
raise Exception("The input is incorrect")
you can have code raising a specific error like
if incorrect_input(value):
raise ValueError("The input is incorrect")
or
class IncorrectInputError(Exception):
pass
if incorrect_input(value):
raise IncorrectInputError("The input is incorrect")
if isinstance(result, bytes): | ||
with open(path, "wb") as f: | ||
f.write(result) | ||
return path | ||
else: | ||
raise Exception(f"GSVI TTS API 请求失败: {result}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (code-quality): 我们发现了这些问题:
- 交换 if/else 分支 (
swap-if-else-branches
) - 删除保护条件后不必要的 else (
remove-unnecessary-else
)
Original comment in English
issue (code-quality): We've found these issues:
- Swap if/else branches (
swap-if-else-branches
) - Remove unnecessary else after guard condition (
remove-unnecessary-else
)
astrbot/core/config/default.py
Outdated
}, | ||
"reply_with_quote": { | ||
"description": "回复时引用消息", | ||
"type": "bool", | ||
"hint": "启用后,机器人回复消息时会引用原消息。实际效果以具体的平台适配器为准。", | ||
"type": "float", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个功能和这个 PR 不太相关,并且好像没看到具体逻辑的变动,可以放到别的 PR 去实现
async with aiohttp.ClientSession() as session: | ||
async with session.request("GET", endpoint, params=params) as response: | ||
if response.status != 200: | ||
return await response.text() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
如果不是 200,这里是返回空响应吗?建议这里直接 raise,然后在调用的时候处理异常
呃,提交上来才发现另一个PR串了 |
已优化 |
Motivation
GPT_SoVIS是非常流行的开源免费、效果强大、可本地部署的TTS模型,之前以Astrbot_plugin_GPT_SoVIS插件形式存在,现在集成到框架中作为“gsv_tts_selfhost”提供TTS服务
Modifications
Check
requirements.txt
和pyproject.toml
文件相应位置。好的,这是翻译成中文的 pull request 总结:
Sourcery 总结
将自托管的 GPT_SoVITS TTS 服务集成到 provider 框架中,并更新相关的默认设置
新特性:
增强功能:
Original summary in English
好的,这是翻译成中文的 pull request 摘要:
Sourcery 总结
将本地托管的 GPT_SoVITS TTS 提供程序集成到框架中,并更新回复配置以使用基于概率的浮点数
新功能:
gsv_tts_selfhost
提供程序适配器,用于 GPT_SoVITS 本地自托管 TTS增强功能:
reply_with_mention
和reply_with_quote
设置从布尔值转换为概率浮点数Original summary in English
Summary by Sourcery
Integrate a locally hosted GPT_SoVITS TTS provider into the framework and update reply configuration to use probability-based floats
New Features:
gsv_tts_selfhost
provider adapter for GPT_SoVITS local self-hosted TTSEnhancements:
reply_with_mention
andreply_with_quote
settings from booleans to probability floats