diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 22a53bb446..31603770b6 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -5,7 +5,7 @@ from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG from astrbot.core.utils.astrbot_path import get_astrbot_data_path -VERSION = "4.25.2" +VERSION = "4.25.0" DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db") PERSONAL_WECHAT_CONFIG_METADATA = { "weixin_oc_base_url": { @@ -120,20 +120,18 @@ "default_personality": "default", "persona_pool": ["*"], "prompt_prefix": "{{prompt}}", - "context_limit_reached_strategy": "llm_compress", # or truncate_by_turns + "context_limit_reached_strategy": "truncate_by_turns", # or llm_compress "llm_compress_instruction": ( "Based on our full conversation history, produce a concise summary of key takeaways and/or project progress.\n" - "The primary goal of this summary is to enable seamless continuation of the work that follows.\n" "1. Systematically cover all core topics discussed and the final conclusion/outcome for each; clearly highlight the latest primary focus.\n" "2. If any tools were used, summarize tool usage (total call count) and extract the most valuable insights from tool outputs.\n" - "3. If any materials (files, documents, code, references) were read during the conversation that may be helpful for subsequent work, list each one with its scope and path.\n" - "4. If there was an initial user goal, state it first and describe the current progress/status.\n" - "5. Write the summary in the user's language.\n" + "3. If there was an initial user goal, state it first and describe the current progress/status.\n" + "4. Write the summary in the user's language.\n" ), - "llm_compress_keep_recent_ratio": 0.15, + "llm_compress_keep_recent": 6, "llm_compress_provider_id": "", - "max_context_length": 50, - "dequeue_context_length": 10, + "max_context_length": -1, + "dequeue_context_length": 1, "streaming_response": False, "show_tool_use_status": False, "show_tool_call_result": False, @@ -254,17 +252,6 @@ "host": "0.0.0.0", "port": 6185, "disable_access_log": True, - "trust_proxy_headers": False, - "auth_rate_limit": { - "enable": True, - "average_interval": 1.0, - "max_burst": 3, - }, - "totp": { - "enable": False, - "secret": "", - "recovery_code_hash": "", - }, "ssl": { "enable": False, "cert_file": "", @@ -1081,7 +1068,7 @@ "id_whitelist": { "type": "list", "items": {"type": "string"}, - "hint": "只处理填写的 ID 发来的消息事件,为空时不启用。可使用 /sid 指令获取在平台上的会话 ID(类似 abc:GroupMessage:123)。管理员可在 WebUI 的平台设置中管理白名单", + "hint": "只处理填写的 ID 发来的消息事件,为空时不启用。可使用 /sid 指令获取在平台上的会话 ID(类似 abc:GroupMessage:123)。管理员可使用 /wl 添加白名单", }, "id_whitelist_log": { "type": "bool", @@ -1250,31 +1237,6 @@ "custom_headers": {"User-Agent": "claude-code/0.1.0"}, "anth_thinking_config": {"type": "", "budget": 0, "effort": ""}, }, - "Xiaomi": { - "id": "xiaomi", - "provider": "xiaomi", - "type": "xiaomi_chat_completion", - "provider_type": "chat_completion", - "enable": True, - "key": [], - "api_base": "https://api.xiaomimimo.com/v1", - "timeout": 120, - "proxy": "", - "custom_headers": {}, - }, - "Xiaomi Token Plan": { - "id": "xiaomi-token-plan", - "provider": "xiaomi-token-plan", - "type": "xiaomi_token_plan", - "provider_type": "chat_completion", - "enable": True, - "key": [], - "api_base": "https://token-plan-cn.xiaomimimo.com/anthropic", - "timeout": 120, - "proxy": "", - "custom_headers": {"User-Agent": "claude-code/0.1.0"}, - "anth_thinking_config": {"type": "", "budget": 0, "effort": ""}, - }, "xAI": { "id": "xai", "provider": "xai", @@ -1778,6 +1740,9 @@ "timeout": 20, "proxy": "", }, + # [PATCH: 2026-06-03] 升级为 V3 API 配置项 + # 旧字段 appid/volcengine_cluster/volcengine_voice_type/volcengine_speed_ratio 已移除 + # 新字段: resource_id(模型选择), speaker(音色), speech_rate(语调), loudness_rate(音量), pitch(音调), emotion(情感), model(子模型) "火山引擎_TTS(API)": { "id": "volcengine_tts", "type": "volcengine_tts", @@ -1785,12 +1750,18 @@ "provider_type": "text_to_speech", "enable": False, "api_key": "", - "appid": "", - "volcengine_cluster": "volcano_tts", - "volcengine_voice_type": "", - "volcengine_speed_ratio": 1.0, - "api_base": "https://openspeech.bytedance.com/api/v1/tts", - "timeout": 20, + "resource_id": "seed-tts-2.0", + "speaker": "", + "format": "mp3", + "sample_rate": 24000, + "bit_rate": 128000, + "speech_rate": 0, + "loudness_rate": 0, + "pitch": 0, + "emotion": "", + "model": "", + "api_base": "https://openspeech.bytedance.com/api/v3/tts/unidirectional", + "timeout": 30, "proxy": "", }, "Gemini TTS": { @@ -2053,8 +2024,8 @@ }, "max_tokens": { "name": "Max Tokens", - "description": "最大词元(Tokens)数", - "hint": "生成的最大词元(Tokens)数。", + "description": "最大令牌数", + "hint": "生成的最大令牌数。", "type": "int", "default": 8192, }, @@ -2203,25 +2174,57 @@ "description": "API Base URL", "type": "string", }, - "volcengine_cluster": { + # [PATCH: 2026-06-03] V3 API 字段元数据 + # 旧字段 volcengine_cluster/volcengine_voice_type/volcengine_speed_ratio/volcengine_volume_ratio 已移除 + "resource_id": { "type": "string", - "description": "火山引擎集群", - "hint": "若使用语音复刻大模型,可选volcano_icl或volcano_icl_concurr,默认使用volcano_tts", + "description": "模型/资源选择", + "hint": "seed-tts-2.0(语音合成2.0) | seed-tts-1.0(语音合成1.0) | seed-icl-2.0(声音复刻2.0) | seed-icl-1.0(声音复刻1.0)", }, - "volcengine_voice_type": { + "speaker": { "type": "string", - "description": "火山引擎音色", - "hint": "输入声音id(Voice_type)", + "description": "发音人", + "hint": "音色ID,如 zh_female_meilinvyou_uranus_bigtts。详见 https://www.volcengine.com/docs/6561/1257544", }, - "volcengine_speed_ratio": { - "type": "float", - "description": "语速设置", - "hint": "语速设置,范围为 0.2 到 3.0,默认值为 1.0", + "format": { + "type": "string", + "description": "音频格式", + "hint": "mp3 / ogg_opus / pcm", }, - "volcengine_volume_ratio": { - "type": "float", - "description": "音量设置", - "hint": "音量设置,范围为 0.0 到 2.0,默认值为 1.0", + "sample_rate": { + "type": "int", + "description": "采样率", + "hint": "8000/16000/22050/24000/32000/44100/48000", + }, + "bit_rate": { + "type": "int", + "description": "比特率", + "hint": "MP3格式建议128000,不传则默认8k音质差", + }, + "speech_rate": { + "type": "int", + "description": "语调(语速)", + "hint": "范围-50~100,默认0。100=2倍速,-50=0.5倍速", + }, + "loudness_rate": { + "type": "int", + "description": "音量", + "hint": "范围-50~100,默认0。100=2倍音量,-50=0.5倍音量", + }, + "pitch": { + "type": "int", + "description": "音调", + "hint": "范围-12~12,默认0。正值升调,负值降调", + }, + "emotion": { + "type": "string", + "description": "情感", + "hint": "如 tender/happy/sad/storytelling。仅部分音色支持", + }, + "model": { + "type": "string", + "description": "模型子类型", + "hint": "仅 seed-icl-2.0 (声音复刻2.0) 生效: seed-icl-2.0-standard(标准) / seed-icl-2.0-expressive(表现力)", }, "azure_tts_voice": { "type": "string", @@ -2998,10 +3001,6 @@ "options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], }, "dashboard.ssl.enable": {"type": "bool"}, - "dashboard.trust_proxy_headers": {"type": "bool"}, - "dashboard.auth_rate_limit.enable": {"type": "bool"}, - "dashboard.auth_rate_limit.average_interval": {"type": "float"}, - "dashboard.auth_rate_limit.max_burst": {"type": "int"}, "dashboard.ssl.cert_file": { "type": "string", "condition": {"dashboard.ssl.enable": True}, @@ -3547,30 +3546,30 @@ "type": "object", "items": { "provider_settings.max_context_length": { - "description": "压缩前最多保留对话轮数", + "description": "最多携带对话轮数", "type": "int", - "hint": "普通会话历史超过该轮数后,才会按下方策略进行持久化截断或 LLM 压缩;请求发送前也会先按该值约束上下文。-1 表示不按轮数限制。", + "hint": "超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条,-1 为不限制", "condition": { "provider_settings.agent_runner_type": "local", }, }, "provider_settings.dequeue_context_length": { - "description": "轮次超限时一次丢弃轮数", + "description": "丢弃对话轮数", "type": "int", - "hint": "当超过“压缩前最多保留对话轮数”且无法使用 LLM 压缩时,一次丢弃多少轮旧对话;请求期截断也会复用该值。", + "hint": "超出最多携带对话轮数时, 一次丢弃的聊天轮数", "condition": { "provider_settings.agent_runner_type": "local", }, }, "provider_settings.context_limit_reached_strategy": { - "description": "历史超限或上下文接近上限时的处理方式", + "description": "超出模型上下文窗口时的处理方式", "type": "string", "options": ["truncate_by_turns", "llm_compress"], "labels": ["按对话轮数截断", "由 LLM 压缩上下文"], "condition": { "provider_settings.agent_runner_type": "local", }, - "hint": "普通会话历史仅在超过“压缩前最多保留对话轮数”后执行该策略;请求发送前也会在上下文 token 接近模型窗口时使用同一策略保护本次请求。", + "hint": "", }, "provider_settings.llm_compress_instruction": { "description": "上下文压缩提示词", @@ -3581,11 +3580,10 @@ "provider_settings.agent_runner_type": "local", }, }, - "provider_settings.llm_compress_keep_recent_ratio": { - "description": "压缩时保留最近上下文比例", - "type": "float", - "slider": {"min": 0, "max": 0.3, "step": 0.01}, - "hint": "按当前上下文 token 数保留最近内容,范围 0-0.3。0.15 表示保留 15%;比例大于 0 时至少保留最后一轮。", + "provider_settings.llm_compress_keep_recent": { + "description": "压缩时保留最近对话轮数", + "type": "int", + "hint": "始终保留的最近 N 轮对话。", "condition": { "provider_settings.context_limit_reached_strategy": "llm_compress", "provider_settings.agent_runner_type": "local", @@ -3595,7 +3593,7 @@ "description": "用于上下文压缩的模型提供商 ID", "type": "string", "_special": "select_provider", - "hint": "留空时使用当前聊天模型进行压缩;如果模型不可用或压缩失败,将回退为“按对话轮数截断”的策略。", + "hint": "留空时将降级为“按对话轮数截断”的策略。", "condition": { "provider_settings.context_limit_reached_strategy": "llm_compress", "provider_settings.agent_runner_type": "local", @@ -4245,34 +4243,6 @@ "type": "bool", "hint": "启用后,WebUI 将直接使用 HTTPS 提供服务。", }, - "dashboard.trust_proxy_headers": { - "description": "信任代理请求头获取客户端 IP", - "type": "bool", - "hint": "关闭时忽略 X-Forwarded-For/X-Real-IP,仅使用连接地址。", - }, - "dashboard.auth_rate_limit.enable": { - "description": "启用登录验证速率限制", - "type": "bool", - "hint": "关闭后将不对登录、TOTP 等身份验证接口进行速率限制。", - }, - "dashboard.auth_rate_limit.average_interval": { - "description": "验证端点速率限制平均间隔(秒)", - "type": "float", - "hint": "两次身份验证请求之间的最小平均间隔时间。例如设置为 1.0 表示每秒最多处理 1 个请求。", - "condition": {"dashboard.auth_rate_limit.enable": True}, - }, - "dashboard.auth_rate_limit.max_burst": { - "description": "验证端点速率限制最大突发数", - "type": "int", - "hint": "允许的瞬时最大突发请求数。例如设置为 3 表示在短时间内最多连续处理 3 个请求。", - "condition": {"dashboard.auth_rate_limit.enable": True}, - }, - "dashboard.totp.enable": { - "description": "启用 WebUI TOTP 双因素认证", - "type": "bool", - "hint": "启用后,登录 WebUI 需要额外输入验证码。", - "_special": "dashboard_totp_manager", - }, "dashboard.ssl.cert_file": { "description": "SSL 证书文件路径", "type": "string", diff --git a/astrbot/core/provider/sources/volcengine_tts.py b/astrbot/core/provider/sources/volcengine_tts.py index 349815907d..77e8747aba 100644 --- a/astrbot/core/provider/sources/volcengine_tts.py +++ b/astrbot/core/provider/sources/volcengine_tts.py @@ -1,7 +1,29 @@ +""" +[PATCH: 2026-06-03] 升级: V1 API → V3 HTTP Chunked 单向流式 API +============================================================ +变更内容: + 1. 鉴权方式: 旧版 (appid + token + cluster) → 新版 (X-Api-Key header) + 2. 接口地址: POST api/v1/tts → POST api/v3/tts/unidirectional (chunked streaming) + 3. 新增模型选择: X-Api-Resource-Id header 可选 seed-tts-2.0 / seed-icl-2.0 等 + 4. 新增音频参数: speech_rate(语速), loudness_rate(音量), pitch(音调), emotion(情感) + 5. 响应解析: 单次 JSON → NDJSON 流式解析 base64 音频 + +参考文档: + - V3 HTTP Chunked API: https://www.volcengine.com/docs/6561/1598757 + - 音色列表: https://www.volcengine.com/docs/6561/1257544 + - API Key 管理: https://www.volcengine.com/docs/6561/2119699 + +易错点排查: + - 音色需与 resource_id 匹配: _uranus_ 后缀音色 → seed-tts-2.0 + - bit_rate 不传则默认 8k, MP3 音质会很差, 建议显式设为 128000 + - pitch 通过 additions.post_process.pitch 传入 (JSON string) +""" + import asyncio import base64 import json import os +import re import traceback import uuid @@ -15,105 +37,263 @@ from ..register import register_provider_adapter +# ============================================================ +# 可用的 resource_id 及其对应模型 (供 WebUI 配置参考) +# ============================================================ +# seed-tts-2.0 → 豆包语音合成模型 2.0 (推荐, 音色带 _uranus_ 后缀) +# seed-tts-1.0 → 豆包语音合成模型 1.0 +# seed-tts-1.0-concurr → 豆包语音合成模型 1.0 并发版 +# seed-icl-2.0 → 声音复刻 2.0 +# seed-icl-1.0 → 声音复刻 1.0 +# seed-icl-1.0-concurr → 声音复刻 1.0 并发版 + + @register_provider_adapter( "volcengine_tts", - "火山引擎 TTS", + "火山引擎 TTS (V3)", provider_type=ProviderType.TEXT_TO_SPEECH, ) class ProviderVolcengineTTS(TTSProvider): + """ + 火山引擎 TTS Provider — V3 HTTP Chunked 单向流式 API 实现 + + 用法: + 在 AstrBot WebUI → 配置 → TTS Provider 中选择 "火山引擎 TTS (V3)" + 填入 API Key (火山引擎新版控制台获取) 和音色 speaker 即可 + """ + def __init__(self, provider_config: dict, provider_settings: dict) -> None: super().__init__(provider_config, provider_settings) + + # === [V3 新] 鉴权: X-Api-Key === + # 从火山引擎新版控制台 (console.volcengine.com/speech/new) 获取 + # 兼容旧配置: 旧版可能把 token 存在 api_key 字段, 升级后需替换为新的 API Key self.api_key = provider_config.get("api_key", "") - self.appid = provider_config.get("appid", "") - self.cluster = provider_config.get("volcengine_cluster", "") - self.voice_type = provider_config.get("volcengine_voice_type", "") - self.speed_ratio = provider_config.get("volcengine_speed_ratio", 1.0) + + # === [V3 新] 模型/资源选择: X-Api-Resource-Id === + # 决定了模型版本和计费方式, 必须与音色匹配 + # 例如 zh_female_meilinvyou_uranus_bigtts 需要 seed-tts-2.0 + self.resource_id = provider_config.get("resource_id", "seed-tts-2.0") + + # === 音色 (兼容旧字段 voice_type) === + # speaker: 发音人标识, 见 https://www.volcengine.com/docs/6561/1257544 + self.speaker = provider_config.get("speaker") or provider_config.get("voice_type", "") + + # === [V3 新] 音频格式 === + # format: mp3 / ogg_opus / pcm (pcm 无文件头, 流式场景推荐) + self.format = provider_config.get("format", "mp3") + # sample_rate: 8000 / 16000 / 22050 / 24000 / 32000 / 44100 / 48000 + self.sample_rate = provider_config.get("sample_rate", 24000) + # bit_rate: MP3 格式强烈建议显式设置 (如 128000), 不传则默认为 8k 音质很差 + self.bit_rate = provider_config.get("bit_rate", 128000) + + # === [V3 新] 语调/语速 === + # speech_rate: -50~100, 默认 0; 100=2倍速, -50=0.5倍速 + self.speech_rate = provider_config.get("speech_rate", 0) + + # === [V3 新] 音量 === + # loudness_rate: -50~100, 默认 0; 100=2倍音量, -50=0.5倍音量 + self.loudness_rate = provider_config.get("loudness_rate", 0) + + # === [V3 新] 情感 === + # emotion: 仅部分音色支持, 见音色列表中的"支持的情感"列 + # 中文可选: happy, sad, angry, surprised, fear, hate, excited, coldness, + # neutral, depressed, lovey-dovey(撒娇), shy, comfort, tension, + # tender, storytelling, radio, magnetic, advertising, vocal-fry, + # ASMR, news, entertainment, dialect + self.emotion = provider_config.get("emotion", "") + # emotion_scale: 情感强度 1~5, 默认 4 (仅当 emotion 设置后生效) + self.emotion_scale = provider_config.get("emotion_scale", 4) + + # === [V3 新] 音调 (通过 additions.post_process.pitch) === + # pitch: -12~12, 默认 0; 正值=升调, 负值=降调 + self.pitch = provider_config.get("pitch", 0) + + # === [V3 新] 模型子类型 (仅声音复刻 2.0 / seed-icl-2.0 生效) === + # seed-tts-2.0-standard: 标准版, 延时更优 + # seed-tts-2.0-expressive: 表现力增强版, 效果更好但可能不稳定 + self.model = provider_config.get("model", "") + + # === 超时时间 (秒) === + self.timeout = provider_config.get("timeout", 30) + + # === API 地址 (可自定义, 如代理) === self.api_base = provider_config.get( "api_base", - "https://openspeech.bytedance.com/api/v1/tts", + "https://openspeech.bytedance.com/api/v3/tts/unidirectional", ) - self.timeout = provider_config.get("timeout", 20) - - def _build_request_payload(self, text: str) -> dict: - return { - "app": { - "appid": self.appid, - "token": self.api_key, - "cluster": self.cluster, - }, - "user": {"uid": str(uuid.uuid4())}, - "audio": { - "voice_type": self.voice_type, - "encoding": "mp3", - "speed_ratio": self.speed_ratio, - "volume_ratio": 1.0, - "pitch_ratio": 1.0, - }, - "request": { - "reqid": str(uuid.uuid4()), + + def _build_payload(self, text: str) -> dict: + """ + [V3] 构建请求体 + + 请求格式参考: https://www.volcengine.com/docs/6561/1598757#_2-2-请求body + """ + # --- audio_params --- + audio_params: dict = { + "format": self.format, + "sample_rate": self.sample_rate, + "speech_rate": self.speech_rate, + "loudness_rate": self.loudness_rate, + } + + if self.bit_rate is not None: + audio_params["bit_rate"] = self.bit_rate + + if self.emotion: + audio_params["emotion"] = self.emotion + audio_params["emotion_scale"] = self.emotion_scale + + # --- additions (JSON string) --- + additions: dict = {} + if self.pitch != 0: + additions["post_process"] = {"pitch": self.pitch} + + # --- 主请求体 --- + payload: dict = { + "user": {"uid": str(uuid.uuid4())[:8]}, + "namespace": "TTS", + "req_params": { "text": text, - "text_type": "plain", - "operation": "query", - "with_frontend": 1, - "frontend_type": "unitTson", + "speaker": self.speaker, + "audio_params": audio_params, }, } + if self.model: + payload["req_params"]["model"] = self.model + + if additions: + payload["req_params"]["additions"] = json.dumps(additions, ensure_ascii=False) + + return payload + async def get_audio(self, text: str) -> str: - """异步方法获取语音文件路径""" + """ + [V3] 调用 HTTP Chunked 单向流式 API 合成语音 + + 返回: 生成的音频文件绝对路径 + + 异常处理: + - HTTP 非 200: 抛出包含状态码和响应体的异常 + - API 返回错误: 抛出包含 code/message 的异常 + - 无音频数据: 抛出提示异常 + """ headers = { "Content-Type": "application/json", - "Authorization": f"Bearer; {self.api_key}", + "X-Api-Key": self.api_key, + "X-Api-Resource-Id": self.resource_id, } - payload = self._build_request_payload(text) + payload = self._build_payload(text) - logger.debug(f"请求头: {headers}") - logger.debug(f"请求 URL: {self.api_base}") - logger.debug(f"请求体: {json.dumps(payload, ensure_ascii=False)[:100]}...") + logger.debug(f"[VolcengineTTS V3] text_len={len(text)}, text_head={repr(text[:60])}") + logger.debug(f"[VolcengineTTS V3] URL={self.api_base}") + logger.debug(f"[VolcengineTTS V3] resource_id={self.resource_id}, speaker={self.speaker}") + logger.debug(f"[VolcengineTTS V3] speech_rate={self.speech_rate}, loudness_rate={self.loudness_rate}, pitch={self.pitch}") + logger.debug(f"[VolcengineTTS V3] model={self.model or '(default)'}") try: async with ( aiohttp.ClientSession() as session, session.post( self.api_base, - data=json.dumps(payload), + json=payload, headers=headers, - timeout=self.timeout, + timeout=aiohttp.ClientTimeout(total=self.timeout), ) as response, ): - logger.debug(f"响应状态码: {response.status}") + # --- 记录响应用于排错 --- + logid = response.headers.get("X-Tt-Logid", "N/A") + logger.debug(f"[VolcengineTTS V3] status={response.status}, logid={logid}") - response_text = await response.text() - logger.debug(f"响应内容: {response_text[:200]}...") + if response.status != 200: + error_body = await response.text() + logger.error(f"[VolcengineTTS V3] HTTP {response.status}: {error_body[:500]}") + raise Exception( + f"火山引擎 TTS 请求失败 (HTTP {response.status}, logid={logid}): {error_body[:300]}" + ) - if response.status == 200: - resp_data = json.loads(response_text) + # --- 读取 chunked 响应 (NDJSON 格式, 每行一个 JSON 对象) --- + # 响应示例: + # {"event":"TTSSentenceStart", ...} + # {"audio":{"data":"//uQx..."}, "event":"TTSSentenceEnd", ...} + # {"event":"TTSResponse", "usage":{...}} + raw_body = b"" + async for chunk in response.content.iter_any(): + if chunk: + raw_body += chunk - if "data" in resp_data: - audio_data = base64.b64decode(resp_data["data"]) + if not raw_body: + raise Exception( + f"火山引擎 TTS 返回空响应 (logid={logid}),请检查 API Key 和 resource_id 是否正确" + ) - temp_dir = get_astrbot_temp_path() - os.makedirs(temp_dir, exist_ok=True) - file_path = os.path.join( - temp_dir, - f"volcengine_tts_{uuid.uuid4()}.mp3", - ) + audio_chunks: list[bytes] = [] + last_event = "" + raw_text = raw_body.decode("utf-8", errors="replace") - loop = asyncio.get_running_loop() - await loop.run_in_executor( - None, - lambda: open(file_path, "wb").write(audio_data), + lines = [l for l in raw_text.strip().split("\n") if l.strip()] + logger.debug(f"[VolcengineTTS V3] Parsing {len(lines)} lines, {len(raw_body)} bytes") + for line in lines: + try: + data = json.loads(line) + except json.JSONDecodeError: + continue + + if "error" in data: + raise Exception( + f"火山引擎 TTS API 错误 (logid={logid}): {json.dumps(data['error'], ensure_ascii=False)}" ) + if "code" in data: + code = data.get("code", 0) + if code not in (0, 20000000): + raise Exception( + f"火山引擎 TTS API 错误 (logid={logid}): " + f"code={code}, message={data.get('message', 'unknown')}" + ) + + event = data.get("event", "") + if event: + last_event = event + + if "data" in data and isinstance(data["data"], str): + b64_str = re.sub(r'\s+', '', data["data"]) + try: + audio_chunks.append(base64.b64decode(b64_str)) + except Exception: + pass + elif "audio" in data and isinstance(data["audio"], dict) and "data" in data["audio"]: + audio_chunks.append(base64.b64decode(data["audio"]["data"])) + + if not audio_chunks: + raise Exception( + f"火山引擎 TTS 未返回音频数据 (logid={logid}, last_event={last_event})。" + f"可能原因: 1) speaker 与 resource_id 不匹配 " + f"2) API Key 对应的服务未开通 " + f"3) 文本内容触发了安全过滤" + ) + + # --- 拼接音频片段并写入文件 --- + full_audio = b"".join(audio_chunks) + + temp_dir = get_astrbot_temp_path() + os.makedirs(temp_dir, exist_ok=True) + file_path = os.path.join(temp_dir, f"volcengine_tts_{uuid.uuid4().hex[:12]}.{self.format}") + + # 异步写入文件 (避免阻塞事件循环) + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, lambda: open(file_path, "wb").write(full_audio)) - return file_path - error_msg = resp_data.get("message", "未知错误") - raise Exception(f"火山引擎 TTS API 返回错误: {error_msg}") - raise Exception( - f"火山引擎 TTS API 请求失败: {response.status}, {response_text}", + logger.info( + f"[VolcengineTTS V3] 合成完成: {file_path} " + f"({len(full_audio)} bytes, {len(audio_chunks)} chunks, logid={logid})" ) + return file_path - except Exception as e: - error_details = traceback.format_exc() - logger.debug(f"火山引擎 TTS 异常详情: {error_details}") - raise Exception(f"火山引擎 TTS 异常: {e!s}") + except aiohttp.ClientError as e: + logger.error(f"[VolcengineTTS V3] 网络异常: {traceback.format_exc()}") + raise Exception(f"火山引擎 TTS 网络请求失败: {e!s}") + except Exception: + # 重新抛出已包含详细信息的异常 + raise diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index 618b95bac4..41c0d60297 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -247,31 +247,31 @@ "provider_settings": { "max_context_length": { "description": "Max Turns Before Compression", - "hint": "Persistent conversation history is truncated or LLM-compressed by the strategy below only after it exceeds this many turns. Request-time contexts are also constrained by this value before sending. -1 means no turn-based limit." + "hint": "Limits history turns before any compression strategy is applied; -1 means no turn-based limit" }, "dequeue_context_length": { "description": "Turns to Discard When Limit Exceeded", - "hint": "When history exceeds 'Max Turns Before Compression' and LLM compression is unavailable, discard this many oldest turns at once. Request-time truncation also reuses this value." + "hint": "Number of old conversation turns to discard at once when the turn limit is exceeded; also used as fallback when compression is unavailable" }, "context_limit_reached_strategy": { - "description": "Handling for History Limits or Context Window Pressure", + "description": "Handling When Context Approaches Model Limit", "labels": [ "Truncate by Turns", "Compress by LLM" ], - "hint": "Persistent conversation history uses this strategy only after exceeding 'Max Turns Before Compression'. Before each request, the same strategy may also protect the in-flight context when tokens approach the model window." + "hint": "This strategy only triggers after turn-based limiting, when context tokens approach the model's window limit. When 'Truncate by Turns' is selected, the oldest N conversation turns will be discarded based on the 'Turns to Discard When Limit Exceeded' setting above. When 'Compress by LLM' is selected, the specified model will be used for context compression." }, "llm_compress_instruction": { "description": "Context Compression Instruction", "hint": "If empty, the default prompt will be used." }, - "llm_compress_keep_recent_ratio": { - "description": "Recent Context Token Ratio to Keep", - "hint": "Keep recent exact context by current context token ratio, from 0-0.3. 0.15 means keeping 15%; values above 0 keep at least the latest round." + "llm_compress_keep_recent": { + "description": "Keep Recent Turns When Compressing", + "hint": "Always keep the most recent N turns of conversation when compressing context." }, "llm_compress_provider_id": { "description": "Model Provider ID for Context Compression", - "hint": "When left empty, the current chat model will be used for compression. If the model is unavailable or compression fails, AstrBot falls back to the 'Truncate by Turns' strategy." + "hint": "When left empty, will fall back to the 'Truncate by Turns' strategy." }, "fallback_max_context_tokens": { "description": "Fallback context window size", @@ -1084,24 +1084,6 @@ "hint": "When disabled, AstrBot will not upload anonymous usage statistics." }, "dashboard": { - "trust_proxy_headers": { - "description": "Trust Proxy Headers for Client IP", - "hint": "When disabled, ignore X-Forwarded-For/X-Real-IP and use the connection address only." - }, - "auth_rate_limit": { - "enable": { - "description": "Enable Login Rate Limiting", - "hint": "When disabled, authentication endpoints (login, TOTP, etc.) will not be rate-limited." - }, - "average_interval": { - "description": "Endpoint Rate Limit Average Interval (seconds)", - "hint": "Minimum average interval between authentication requests. For example, 1.0 means at most 1 request per second." - }, - "max_burst": { - "description": "Endpoint Rate Limit Max Burst", - "hint": "Maximum number of consecutive burst requests allowed. For example, 3 allows up to 3 requests in a short burst." - } - }, "ssl": { "enable": { "description": "Enable WebUI HTTPS", @@ -1119,52 +1101,6 @@ "description": "SSL CA Certificate File Path", "hint": "Optional. Path to CA certificate file." } - }, - "totp": { - "enable": { - "description": "Enable WebUI TOTP", - "hint": "When enabled, a TOTP code is required during dashboard login." - }, - "manage": "Manage", - "configuration": "TOTP", - "statusPending": "Setup required", - "statusEnabled": "Enabled", - "setupRequiredHint": "TOTP is enabled but not yet configured. Click Manage to complete setup.", - "setupTitle": "Set up TOTP", - "setupSubtitle": "Scan this QR code in your authenticator app, then enter a verification code.", - "setupConfirm": "Verify and continue", - "activeSubtitle": "Use this QR code or secret to add another authenticator device.", - "rotateTitle": "Rotate TOTP Secret", - "rotateSubtitle": "Generate a new secret, then enter a code from your authenticator to confirm the replacement.", - "rotate": "Rotate", - "rotateRecovery": "Rotate Recovery Code", - "rotateConfirm": "Confirm Rotation", - "rotateCancel": "Cancel", - "rotateCode": "Verification Code", - "rotateCodeHint": "Enter the code from your authenticator app to confirm the new key.", - "rotateError": "Invalid code, please try again.", - "recoveryTitle": "Recovery Codes", - "recoverySubtitle": "This recovery code is shown once. Save it before continuing.", - "recoveryWarning": "If lost, account access cannot be restored through normal means.", - "recoveryAcknowledge": "I have saved my recovery codes", - "recoveryClose": "Done", - "disableTitle": "Disable TOTP", - "disableSubtitle": "Enter a verification code to disable two-factor authentication.", - "disableRecoverySubtitle": "Enter a recovery code to disable two-factor authentication.", - "disableCode": "Verification Code", - "disableRecoveryCode": "Recovery Code", - "disableConfirm": "Disable", - "disableCancel": "Cancel", - "disableError": "Verification failed. Please try again.", - "disableUseRecovery": "Can't use TOTP?", - "disableUseCode": "Use verification code", - "configSaveTitle": "Two-Factor Verification", - "configSaveSubtitle": "Enter a verification code to change protected configuration.", - "configSaveRotationHint": "When rotating TOTP keys, both the old and new verification codes are accepted (allowed once per rotation operation).", - "configSaveCode": "Verification Code", - "configSaveConfirm": "Continue", - "configSaveCancel": "Cancel", - "configSaveError": "Verification failed. Please try again." } }, "timezone": { @@ -1385,21 +1321,45 @@ "gemini_embedding": { "hint": "Gemini Embedding does not require manually adding /v1beta." }, - "volcengine_cluster": { - "description": "Volcengine cluster", - "hint": "For voice cloning models, choose volcano_icl or volcano_icl_concurr; default is volcano_tts." + "resource_id": { + "description": "Model / Resource", + "hint": "seed-tts-2.0 (TTS 2.0) | seed-tts-1.0 (TTS 1.0) | seed-icl-2.0 (Voice Cloning 2.0) | seed-icl-1.0 (Voice Cloning 1.0)" }, - "volcengine_voice_type": { - "description": "Volcengine voice", - "hint": "Enter voice id (Voice_type)." + "speaker": { + "description": "Speaker", + "hint": "Voice ID, e.g. zh_female_meilinvyou_uranus_bigtts. See https://www.volcengine.com/docs/6561/1257544" }, - "volcengine_speed_ratio": { - "description": "Speech rate", - "hint": "Speech rate, range 0.2 to 3.0, default 1.0." + "format": { + "description": "Audio Format", + "hint": "mp3 / ogg_opus / pcm" }, - "volcengine_volume_ratio": { + "sample_rate": { + "description": "Sample Rate", + "hint": "8000/16000/22050/24000/32000/44100/48000" + }, + "bit_rate": { + "description": "Bit Rate", + "hint": "Recommended 128000 for MP3. Default 8k if omitted, resulting in poor quality" + }, + "speech_rate": { + "description": "Speech Rate", + "hint": "Range -50~100, default 0. 100 = 2x speed, -50 = 0.5x speed" + }, + "loudness_rate": { "description": "Volume", - "hint": "Volume, range 0.0 to 2.0, default 1.0." + "hint": "Range -50~100, default 0. 100 = 2x volume, -50 = 0.5x volume" + }, + "pitch": { + "description": "Pitch", + "hint": "Range -12~12, default 0. Positive = higher, negative = lower" + }, + "emotion": { + "description": "Emotion", + "hint": "e.g. tender/happy/sad/storytelling. Only supported by certain voices" + }, + "model": { + "description": "Model Subtype", + "hint": "Only for seed-icl-2.0 (Voice Cloning 2.0): seed-icl-2.0-standard / seed-icl-2.0-expressive" }, "azure_tts_voice": { "description": "Voice style", diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json index c42a3313a5..fb8ea47cce 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json +++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json @@ -246,32 +246,32 @@ "description": "Стратегия управления контекстом", "provider_settings": { "max_context_length": { - "description": "Макс. раундов перед сжатием", - "hint": "Постоянная история диалога обрезается или сжимается LLM по стратегии ниже только после превышения этого числа раундов. Контекст перед запросом также ограничивается этим значением. -1 означает без ограничений по раундам." + "description": "Макс. количество раундов диалога", + "hint": "При превышении удаляются старые сообщения. 1 раунд = 1 пара запрос-ответ. -1 означает без ограничений." }, "dequeue_context_length": { - "description": "Раундов для удаления при превышении лимита", - "hint": "Когда история превышает лимит раундов и LLM-сжатие недоступно, за один раз удаляется это число самых старых раундов. Обрезка перед запросом также использует это значение." + "description": "Кол-во удаляемых раундов", + "hint": "Сколько раундов удалять за один раз при достижении лимита." }, "context_limit_reached_strategy": { - "description": "Действие при лимите истории или давлении окна контекста", + "description": "Действие при переполнении окна контекста", "labels": [ "Обрезать по раундам", "Сжать с помощью LLM" ], - "hint": "Постоянная история диалога использует эту стратегию только после превышения лимита раундов. Перед каждым запросом та же стратегия может защищать текущий контекст, когда токены приближаются к окну модели." + "hint": "При выборе 'Обрезать' удаляются старые сообщения. При выборе 'Сжать' используется модель для суммаризации контекста." }, "llm_compress_instruction": { "description": "Инструкция для сжатия контекста", "hint": "Если пусто, используется промпт по умолчанию." }, - "llm_compress_keep_recent_ratio": { - "description": "Доля последних токенов контекста при сжатии", - "hint": "Сохраняет последние сообщения по доле текущих токенов контекста, от 0 до 0.3. 0.15 означает 15%; значение выше 0 сохраняет как минимум последний раунд." + "llm_compress_keep_recent": { + "description": "Сохранять последние раунды при сжатии", + "hint": "Всегда оставлять последние N раундов диалога без изменений при сжатии." }, "llm_compress_provider_id": { "description": "Модель для сжатия контекста", - "hint": "Если не выбрано, для сжатия используется текущая модель чата. Если модель недоступна или сжатие завершается ошибкой, AstrBot откатывается к обрезке по раундам." + "hint": "Если не выбрано, произойдет откат к стратегии удаления сообщений." }, "fallback_max_context_tokens": { "description": "Запасной размер окна контекста", @@ -1085,24 +1085,6 @@ "hint": "После отключения AstrBot не будет отправлять анонимные данные об использовании." }, "dashboard": { - "trust_proxy_headers": { - "description": "Доверять прокси-заголовкам для IP клиента", - "hint": "Если выключено, X-Forwarded-For/X-Real-IP игнорируются и используется только адрес соединения." - }, - "auth_rate_limit": { - "enable": { - "description": "Включить ограничение скорости входа", - "hint": "Если выключено, конечные точки аутентификации (вход, TOTP и т.д.) не будут ограничены по скорости." - }, - "average_interval": { - "description": "Средний интервал ограничения скорости конечных точек (сек)", - "hint": "Минимальный средний интервал между запросами аутентификации. Например, 1.0 означает не более 1 запроса в секунду." - }, - "max_burst": { - "description": "Максимальный всплеск ограничения скорости конечных точек", - "hint": "Максимальное количество последовательных всплесков запросов. Например, 3 допускает до 3 запросов за короткий всплеск." - } - }, "ssl": { "enable": { "description": "Включить HTTPS для WebUI", @@ -1120,52 +1102,6 @@ "description": "Путь к сертификату CA SSL", "hint": "Опционально. Путь к сертификату CA." } - }, - "totp": { - "enable": { - "description": "Включить TOTP для WebUI", - "hint": "Когда включено, TOTP-код требуется для входа в панель управления." - }, - "manage": "Управление", - "configuration": "TOTP", - "statusPending": "Требуется настройка", - "statusEnabled": "Включено", - "setupRequiredHint": "TOTP включен, но ещё не настроен. Откройте «Управление», чтобы завершить настройку.", - "setupTitle": "Настройка TOTP", - "setupSubtitle": "Отсканируйте QR-код в приложении-аутентификаторе и введите код подтверждения.", - "setupConfirm": "Подтвердить и продолжить", - "activeSubtitle": "Используйте этот QR-код или секрет для добавления нового устройства-аутентификатора.", - "rotateTitle": "Смена секрета TOTP", - "rotateSubtitle": "Сгенерируйте новый секрет и подтвердите его перед заменой текущего.", - "rotate": "Сменить", - "rotateRecovery": "Сменить код восстановления", - "rotateConfirm": "Подтвердить смену", - "rotateCancel": "Отмена", - "rotateCode": "Код подтверждения", - "rotateCodeHint": "Введите код из приложения-аутентификатора для подтверждения нового ключа.", - "rotateError": "Неверный код, попробуйте снова.", - "recoveryTitle": "Коды восстановления", - "recoverySubtitle": "Этот код показывается один раз. Сохраните его перед продолжением.", - "recoveryWarning": "При утере этого кода восстановить доступ к учётной записи обычными средствами будет невозможно.", - "recoveryAcknowledge": "Я сохранил(а) коды восстановления", - "recoveryClose": "Готово", - "disableTitle": "Отключить TOTP", - "disableSubtitle": "Введите код подтверждения для отключения двухфакторной аутентификации.", - "disableRecoverySubtitle": "Введите код восстановления для отключения двухфакторной аутентификации.", - "disableCode": "Код подтверждения", - "disableRecoveryCode": "Код восстановления", - "disableConfirm": "Отключить", - "disableCancel": "Отмена", - "disableError": "Ошибка проверки. Попробуйте снова.", - "disableUseRecovery": "Не можете использовать TOTP?", - "disableUseCode": "Использовать код подтверждения", - "configSaveTitle": "Двухфакторная проверка", - "configSaveSubtitle": "Введите код подтверждения для изменения защищённой конфигурации.", - "configSaveRotationHint": "При смене TOTP-ключа принимаются как старый, так и новый коды подтверждения (только однократно при смене).", - "configSaveCode": "Код подтверждения", - "configSaveConfirm": "Продолжить", - "configSaveCancel": "Отмена", - "configSaveError": "Ошибка проверки. Попробуйте снова." } }, "timezone": { @@ -1382,21 +1318,45 @@ "gemini_embedding": { "hint": "Gemini Embedding не требует ручного добавления /v1beta." }, - "volcengine_cluster": { - "description": "Кластер Volcengine", - "hint": "Для моделей клонирования голоса выберите volcano_icl или volcano_icl_concurr; по умолчанию volcano_tts." + "resource_id": { + "description": "Модель / Ресурс", + "hint": "seed-tts-2.0 (TTS 2.0) | seed-tts-1.0 (TTS 1.0) | seed-icl-2.0 (Клонирование 2.0) | seed-icl-1.0 (Клонирование 1.0)" }, - "volcengine_voice_type": { - "description": "Голос Volcengine", - "hint": "Введите ID голоса (Voice_type)." + "speaker": { + "description": "Диктор", + "hint": "ID голоса, напр. zh_female_meilinvyou_uranus_bigtts. См. https://www.volcengine.com/docs/6561/1257544" }, - "volcengine_speed_ratio": { + "format": { + "description": "Формат аудио", + "hint": "mp3 / ogg_opus / pcm" + }, + "sample_rate": { + "description": "Частота дискретизации", + "hint": "8000/16000/22050/24000/32000/44100/48000" + }, + "bit_rate": { + "description": "Битрейт", + "hint": "Рекомендуется 128000 для MP3. По умолчанию 8k — плохое качество" + }, + "speech_rate": { "description": "Скорость речи", - "hint": "Скорость речи, от 0.2 до 3.0, по умолчанию 1.0." + "hint": "Диапазон -50~100, по умолчанию 0. 100 = 2x, -50 = 0.5x" }, - "volcengine_volume_ratio": { + "loudness_rate": { "description": "Громкость", - "hint": "Громкость, от 0.0 до 2.0, по умолчанию 1.0." + "hint": "Диапазон -50~100, по умолчанию 0. 100 = 2x, -50 = 0.5x" + }, + "pitch": { + "description": "Тон", + "hint": "Диапазон -12~12, по умолчанию 0. + = выше, - = ниже" + }, + "emotion": { + "description": "Эмоция", + "hint": "Напр. tender/happy/sad/storytelling. Поддерживается не всеми голосами" + }, + "model": { + "description": "Подтип модели", + "hint": "Только для seed-icl-2.0 (Voice Cloning 2.0): seed-icl-2.0-standard / seed-icl-2.0-expressive" }, "azure_tts_voice": { "description": "Стиль голоса", diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index 200b3b9fe1..5561eceb2c 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -249,31 +249,31 @@ "provider_settings": { "max_context_length": { "description": "压缩前最多保留对话轮数", - "hint": "普通会话历史超过该轮数后,才会按下方策略进行持久化截断或 LLM 压缩;请求发送前也会先按该值约束上下文。-1 表示不按轮数限制。" + "hint": "无论选择截断还是 LLM 压缩,都会先按该值限制历史轮数;-1 表示不按轮数限制" }, "dequeue_context_length": { "description": "轮次超限时一次丢弃轮数", - "hint": "当超过\"压缩前最多保留对话轮数\"且无法使用 LLM 压缩时,一次丢弃多少轮旧对话;请求期截断也会复用该值。" + "hint": "当超过\"压缩前最多保留对话轮数\"时,一次丢弃多少轮旧对话;同时也可能作为压缩不可用时的回退截断参数" }, "context_limit_reached_strategy": { - "description": "历史超限或上下文接近上限时的处理方式", + "description": "模型上下文接近上限后的处理方式", "labels": [ "按对话轮数截断", "由 LLM 压缩上下文" ], - "hint": "普通会话历史仅在超过\"压缩前最多保留对话轮数\"后执行该策略;请求发送前也会在上下文 token 接近模型窗口时使用同一策略保护本次请求。" + "hint": "该策略只会在完成轮次限制后,且上下文 token 接近模型窗口上限时触发。当按对话轮数截断时,会根据上面\"轮次超限时一次丢弃轮数\"的配置丢弃最旧的 N 轮对话。当由 LLM 压缩上下文时,会使用指定的模型进行上下文压缩。" }, "llm_compress_instruction": { "description": "上下文压缩提示词", "hint": "如果为空则使用默认提示词。" }, - "llm_compress_keep_recent_ratio": { - "description": "压缩时保留最近上下文比例", - "hint": "按当前上下文 token 数保留最近内容,范围 0-0.3。0.15 表示保留 15%;比例大于 0 时至少保留最后一轮。" + "llm_compress_keep_recent": { + "description": "压缩时保留最近对话轮数", + "hint": "始终保留的最近 N 轮对话。" }, "llm_compress_provider_id": { "description": "用于上下文压缩的模型提供商 ID", - "hint": "留空时使用当前聊天模型进行压缩;如果模型不可用或压缩失败,将回退为\"按对话轮数截断\"的策略。" + "hint": "留空时将降级为\"按对话轮数截断\"的策略。" }, "fallback_max_context_tokens": { "description": "上下文窗口兜底值", @@ -1086,24 +1086,6 @@ "hint": "禁用后,AstrBot 将不再上传匿名使用统计数据。" }, "dashboard": { - "trust_proxy_headers": { - "description": "信任代理请求头获取客户端 IP", - "hint": "关闭时忽略 X-Forwarded-For/X-Real-IP,仅使用连接地址。" - }, - "auth_rate_limit": { - "enable": { - "description": "启用登录验证速率限制", - "hint": "关闭后将不对登录、TOTP 等身份验证接口进行速率限制。" - }, - "average_interval": { - "description": "验证端点速率限制平均间隔(秒)", - "hint": "两次身份验证请求之间的最小平均间隔时间。例如设置为 1.0 表示每秒最多处理 1 个请求。" - }, - "max_burst": { - "description": "验证端点速率限制最大突发数", - "hint": "允许的瞬时最大突发请求数。例如设置为 3 表示在短时间内最多连续处理 3 个请求。" - } - }, "ssl": { "enable": { "description": "启用 WebUI HTTPS", @@ -1121,52 +1103,6 @@ "description": "SSL CA 证书文件路径", "hint": "可选。用于指定 CA 证书文件路径。" } - }, - "totp": { - "enable": { - "description": "启用 WebUI TOTP 双因素认证", - "hint": "启用后,登录 WebUI 需要额外输入验证码。" - }, - "manage": "管理", - "configuration": "TOTP", - "statusPending": "需完成设置", - "statusEnabled": "已启用", - "setupRequiredHint": "TOTP 已开启但尚未完成配置,请点击“管理”完成初始化。", - "setupTitle": "设置 TOTP", - "setupSubtitle": "请使用认证器应用扫描二维码,然后输入验证码。", - "setupConfirm": "验证并继续", - "activeSubtitle": "可使用此二维码和密钥添加新的认证器设备。", - "rotateTitle": "更换 TOTP 密钥", - "rotateSubtitle": "生成新密钥并完成验证后,将替换当前密钥。", - "rotate": "更换密钥", - "rotateRecovery": "更换恢复码", - "rotateConfirm": "确认更换", - "rotateCancel": "取消", - "rotateCode": "验证码", - "rotateCodeHint": "输入认证器应用中的验证码以确认新密钥。", - "rotateError": "验证码无效,请重试。", - "recoveryTitle": "恢复码", - "recoverySubtitle": "恢复码仅展示一次,请在继续前妥善保存。", - "recoveryWarning": "若恢复码丢失将无法通过常规途径恢复账户访问权限。", - "recoveryAcknowledge": "我已保存恢复码", - "recoveryClose": "完成", - "disableTitle": "关闭 TOTP", - "disableSubtitle": "输入验证码以确认关闭双因素认证。", - "disableRecoverySubtitle": "输入恢复码以确认关闭双因素认证。", - "disableCode": "验证码", - "disableRecoveryCode": "恢复码", - "disableConfirm": "确认关闭", - "disableCancel": "取消", - "disableError": "验证失败,请重试。", - "disableUseRecovery": "无法使用TOTP?", - "disableUseCode": "使用验证码", - "configSaveTitle": "两步验证", - "configSaveSubtitle": "输入验证码以更改受保护的配置。", - "configSaveRotationHint": "轮换 TOTP 密钥时,轮换前和轮换后的验证码均可用(仅轮换操作中单次允许)。", - "configSaveCode": "验证码", - "configSaveConfirm": "继续", - "configSaveCancel": "取消", - "configSaveError": "验证失败,请重试。" } }, "timezone": { @@ -1273,8 +1209,8 @@ "name": "Top-p" }, "max_tokens": { - "description": "最大词元(Tokens)数", - "hint": "生成的最大词元(Tokens)数。", + "description": "最大令牌数", + "hint": "生成的最大令牌数。", "name": "Max Tokens" } } @@ -1387,21 +1323,45 @@ "gemini_embedding": { "hint": "Gemini Embedding 无需手动添加 /v1beta。" }, - "volcengine_cluster": { - "description": "火山引擎集群", - "hint": "若使用语音复刻大模型,可选volcano_icl或volcano_icl_concurr,默认使用volcano_tts" + "resource_id": { + "description": "模型/资源选择", + "hint": "seed-tts-2.0(语音合成2.0) | seed-tts-1.0(语音合成1.0) | seed-icl-2.0(声音复刻2.0) | seed-icl-1.0(声音复刻1.0)" }, - "volcengine_voice_type": { - "description": "火山引擎音色", - "hint": "输入声音id(Voice_type)" + "speaker": { + "description": "发音人", + "hint": "音色ID,如 zh_female_meilinvyou_uranus_bigtts。详见 https://www.volcengine.com/docs/6561/1257544" }, - "volcengine_speed_ratio": { - "description": "语速设置", - "hint": "语速设置,范围为 0.2 到 3.0,默认值为 1.0" + "format": { + "description": "音频格式", + "hint": "mp3 / ogg_opus / pcm" + }, + "sample_rate": { + "description": "采样率", + "hint": "8000/16000/22050/24000/32000/44100/48000" + }, + "bit_rate": { + "description": "比特率", + "hint": "MP3格式建议128000,不传则默认8k音质差" }, - "volcengine_volume_ratio": { - "description": "音量设置", - "hint": "音量设置,范围为 0.0 到 2.0,默认值为 1.0" + "speech_rate": { + "description": "语调(语速)", + "hint": "范围-50~100,默认0。100=2倍速,-50=0.5倍速" + }, + "loudness_rate": { + "description": "音量", + "hint": "范围-50~100,默认0。100=2倍音量,-50=0.5倍音量" + }, + "pitch": { + "description": "音调", + "hint": "范围-12~12,默认0。正值升调,负值降调" + }, + "emotion": { + "description": "情感", + "hint": "如 tender/happy/sad/storytelling。仅部分音色支持" + }, + "model": { + "description": "模型子类型", + "hint": "仅 seed-icl-2.0 (声音复刻2.0) 生效: seed-icl-2.0-standard(标准) / seed-icl-2.0-expressive(表现力)" }, "azure_tts_voice": { "description": "音色设置",