diff --git a/VOLCANO_VOICE_README.md b/VOLCANO_VOICE_README.md new file mode 100644 index 000000000..4fe8c2122 --- /dev/null +++ b/VOLCANO_VOICE_README.md @@ -0,0 +1,195 @@ +# 火山语音服务配置指南 + +本文档提供了关于火山语音服务的配置和常见问题解决方案。 + +## 环境变量配置 + +火山语音服务需要以下环境变量: + +``` +# 火山语音服务配置 +VOLCANO_APP_ID=your_volcano_app_id_here # 应用ID +VOLCANO_APP_KEY=your_volcano_app_key_here # 应用密钥/Token +VOLCANO_CLUSTER=your_volcano_cluster_here # 集群区域,例如: cn-beijing + +# 语音合成配置 +VOLCANO_VOICE_TYPE=zh_female_wanqudashu_moon_bigtts # 默认语音类型 - 湾区大叔 +``` + +请确保在 `.env` 文件中正确设置这些变量。 + +## 常见问题解决 + +### 1. TTS资源授权错误 + +如果遇到以下错误: + +``` +语音类型授权错误: 您可能没有权限使用当前选择的语音类型 +``` + +**解决方案**: +- 尝试使用基础语音类型,如 `zh_male_qingse_common` 或 `zh_female_qingse_common` +- 确保您的火山引擎账户已开通语音合成服务 +- 检查应用ID和密钥是否正确 + +### 2. 语音识别认证错误 + +如果遇到以下错误: + +``` +authentication signature from request: 'Authorization' header: invalid auth token +``` + +**解决方案**: + +#### 标准语音识别SDK (API v2) +- 确保 `VOLCANO_APP_KEY` 格式正确,这是一个完整的令牌 +- **必须**在Token前添加 `Bearer;` 前缀(注意使用分号而非空格) +- 检查应用ID和密钥是否匹配 +- 确保您的火山引擎账户已开通语音识别服务 +- 使用正确的API路径: `/api/v2/asr` + +#### 大模型流式识别SDK (API v3) +- 使用正确的API路径: `/api/v3/sauc/bigmodel` +- **不要**在Token前添加Bearer前缀 +- 设置正确的资源ID (`RESOURCE_ID_STRING`) +- 设置协议类型为 `PROTOCOL_TYPE_SEED` +- 确保您的火山引擎账户已开通大模型流式语音识别服务 + +### 3. WebSocket连接错误 + +如果遇到以下错误: + +``` +Error during WebSocket handshake: Unexpected response code: 400 +``` + +**解决方案**: +- 确保网络连接正常,可以访问 `openspeech.bytedance.com` +- **集群区域设置非常重要**,必须设置正确的 `VOLCANO_CLUSTER` 环境变量 +- 默认使用 `cn-beijing`,但您的账户可能需要使用其他区域,如 `cn-shanghai` 或 `cn-guangzhou` +- 如果使用默认区域出现错误,请尝试切换到其他区域 +- 确保您的账户已开通相应的语音识别服务 +- 检查请求参数格式是否正确 +- 对于标准语音识别SDK (API v2),确保Token前添加了 `Bearer;` 前缀 +- 如果问题仍然存在,请联系火山引擎技术支持 + +### 4. 检查配置工具 + +我们提供了两个工具来检查火山语音服务的配置: + +1. **检查TTS配置**: + ``` + flutter run lib/tools/check_volcano_config.dart + ``` + +2. **检查语音识别配置**: + ``` + flutter run lib/tools/check_volcano_asr_config.dart + ``` + +这些工具将帮助您验证环境变量、网络连接和服务授权是否正确。 + +## 离线TTS支持 + +我们的应用支持离线TTS功能,当在线TTS失败时会自动切换到离线模式。离线模式支持基础语音类型: +- `zh_male_qingse_common`(基础男声) +- `zh_female_qingse_common`(基础女声) + +要使用离线TTS,您可以: +1. 在TTS测试页面选择基础语音类型 +2. 当在线合成失败时,系统会自动尝试使用离线合成 + +## 标准语音识别SDK配置 (API v2) + +标准语音识别SDK是火山语音服务的基础版本,配置相对简单。 + +### 关键配置点 +1. **API路径**:使用 `/api/v2/asr` +2. **认证方式**: + - 必须在Token前添加 `Bearer;` 前缀(注意使用分号而非空格) + - 不需要设置资源ID +3. **集群区域**:确保设置正确的集群区域,如 `cn-beijing` + - 集群区域必须与您的账户配置匹配 + - 如果遇到WebSocket握手错误,尝试切换到其他区域 + +### 配置示例 +```kotlin +// 设置API路径 +engine.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING, "/api/v2/asr"); + +// 设置认证信息 +engine.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, "YOUR_APP_ID"); +engine.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, "Bearer;YOUR_APP_KEY"); // 必须添加Bearer;前缀 + +// 设置集群区域 +engine.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_CLUSTER_STRING, "cn-beijing"); +``` + +## 大模型流式识别SDK配置 (API v3) + +从2024年2月26日起,火山语音服务提供了新的大模型流式识别SDK。如果您使用的是这个新版本,请注意以下配置差异: + +### 版本信息 +- Android: `com.bytedance.speechengine:speechengine_asr_tob:1.1.7` +- iOS: `pod 'SpeechEngineAsrToB', '1.1.7'` + +### 关键配置差异 +1. **API路径**:使用 `/api/v3/sauc/bigmodel` 而非旧版的 `/api/v2/asr` +2. **认证方式**: + - 不需要在Token前添加 `Bearer` 前缀 + - 需要设置资源ID (`RESOURCE_ID_STRING`) +3. **协议类型**:需要设置为 `PROTOCOL_TYPE_SEED` +4. **集群区域**:确保设置正确的集群区域,如 `cn-beijing` + - 集群区域必须与您的账户配置匹配 + - 如果遇到WebSocket握手错误,尝试切换到其他区域 + +### 配置示例 +```kotlin +// 设置API路径 +mSpeechEngine.setOptionString(SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING, "/api/v3/sauc/bigmodel"); + +// 设置认证信息 +mSpeechEngine.setOptionString(SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, "YOUR_APP_ID"); +mSpeechEngine.setOptionString(SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, "YOUR_APP_KEY"); // 不需要Bearer前缀 + +// 设置资源ID +mSpeechEngine.setOptionString(SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING, "YOUR_RESOURCE_ID"); + +// 设置协议类型 +mSpeechEngine.setOptionInt(SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT, SpeechEngineDefines.PROTOCOL_TYPE_SEED); + +// 设置集群区域 +mSpeechEngine.setOptionString(SpeechEngineDefines.PARAMS_KEY_ASR_CLUSTER_STRING, "cn-beijing"); + +// 设置ASR请求参数 +mSpeechEngine.setOptionString(SpeechEngineDefines.PARAMS_KEY_ASR_REQ_PARAMS_STRING, + "{"force_to_speech_time":0, "end_window_size":800}"); +``` + +## 支持的语音类型 + +我们支持多种语音类型,包括: + +### 趣味方言 +- 湾区大叔 (`zh_female_wanqudashu_moon_bigtts`) +- 呆萌川妹 (`zh_female_daimengchuanmei_moon_bigtts`) +- 广州德哥 (`zh_male_guozhoudege_moon_bigtts`) +- 北京小爷 (`zh_male_beijingxiaoye_moon_bigtts`) +- 浩宇小哥 (`zh_male_haoyuxiaoge_moon_bigtts`) + +### 通用场景 +- 少年梓辛/Brayan (`zh_male_shaonianzixin_moon_bigtts`) + +### 角色扮演 +- 魅力女友 (`zh_female_meilinvyou_moon_bigtts`) +- 深夜播客 (`zh_male_shenyeboke_moon_bigtts`) +- 柔美女友 (`zh_female_sajiaonvyou_moon_bigtts`) +- 撒娇学妹 (`zh_female_yuanqinvyou_moon_bigtts`) + +### 基础语音类型 +- 基础男声 (`zh_male_qingse_common`) +- 基础女声 (`zh_female_qingse_common`) +- 高级男声 (`zh_male_M392_conversation_wvae_bigtts`) +- 高级女声 (`zh_female_F392_conversation_wvae_bigtts`) \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index 8fbca00db..2435c5ef8 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt @@ -17,18 +17,18 @@ class MainActivity: AudioServiceActivity() { private val SPEECH_RECOGNITION_EVENT_CHANNEL = "com.example.deep_voice/speech_recognition_events" private val VOLCANO_TTS_CHANNEL = "com.example.deep_voice/volcano_tts" private val TAG = "MainActivity" - private val speechHelper = SpeechRecognitionHelper() - private val volcanoTtsHelper = VolcanoTtsHelper() + private lateinit var speechHelper: SpeechRecognitionHelper + private lateinit var volcanoTtsHelper: VolcanoTtsHelper private var eventSink: EventChannel.EventSink? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // 设置火山语音合成Helper的上下文 - volcanoTtsHelper.setContext(applicationContext) + // Initialize speechHelper with context + speechHelper = SpeechRecognitionHelper(applicationContext) - // 设置火山语音识别Helper的上下文 - speechHelper.setContext(applicationContext) + // Initialize volcanoTtsHelper with context + volcanoTtsHelper = VolcanoTtsHelper(applicationContext) } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { @@ -40,6 +40,8 @@ class MainActivity: AudioServiceActivity() { "initialize" -> { val subscriptionKey = call.argument("subscriptionKey") val serviceRegion = call.argument("serviceRegion") + val resourceId = call.argument("resourceId") + val cluster = call.argument("cluster") ?: "cn-beijing" if (subscriptionKey == null || serviceRegion == null) { result.error("INVALID_ARGUMENTS", "subscriptionKey and serviceRegion are required", null) @@ -47,8 +49,13 @@ class MainActivity: AudioServiceActivity() { } try { - Log.d(TAG, "初始化语音识别服务,APP_ID: ${subscriptionKey.take(3)}***,APP_KEY: ${serviceRegion.take(5)}...") - val success = speechHelper.initialize(subscriptionKey, serviceRegion) + Log.d(TAG, "初始化语音识别服务,APP_ID: ${subscriptionKey.take(3)}***,APP_KEY: ${serviceRegion.take(5)}***") + Log.d(TAG, "APP_ID长度: ${subscriptionKey.length}, APP_KEY长度: ${serviceRegion.length}") + Log.d(TAG, "使用大模型流式识别SDK配置") + Log.d(TAG, "资源ID: ${resourceId ?: subscriptionKey}") + Log.d(TAG, "集群区域: $cluster") + + val success = speechHelper.initialize(subscriptionKey, serviceRegion, cluster) Log.d(TAG, "语音识别服务初始化${if (success) "成功" else "失败"}") result.success(success) } catch (e: Exception) { diff --git a/android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt index f737c680e..19f3324cf 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt @@ -15,16 +15,17 @@ import java.util.concurrent.TimeUnit * * 该类封装了火山语音SDK的语音识别功能,提供简单的接口供Flutter调用 */ -class SpeechRecognitionHelper : SpeechEngine.SpeechListener { +class SpeechRecognitionHelper(context: Context) : SpeechEngine.SpeechListener { private val TAG = "SpeechRecognitionHelper" private var mSpeechEngine: SpeechEngine? = null private var mSpeechEngineHandler: Long = -1 private var isInitialized = false - private var applicationContext: Context? = null + private var applicationContext: Context? = context.applicationContext // 存储应用ID和密钥,以便在错误处理中使用 private var appId: String = "" private var appKey: String = "" + private var cluster: String = "" // 移除默认值 // 同步识别相关变量 private var mRecognitionLatch: CountDownLatch? = null @@ -40,15 +41,19 @@ class SpeechRecognitionHelper : SpeechEngine.SpeechListener { /** * 初始化语音识别引擎 - * - * @param subscriptionKey 订阅密钥 - * @param serviceRegion 服务区域 + * @param appId 应用ID + * @param token 应用密钥/令牌 + * @param cluster 集群区域 + * @return 初始化是否成功 */ - fun initialize(subscriptionKey: String, serviceRegion: String): Boolean { + fun initialize(appId: String, token: String, cluster: String): Boolean { try { - // 保存应用ID和密钥,以便在错误处理中使用 - this.appId = subscriptionKey - this.appKey = serviceRegion + // 设置基本参数 + this.appId = appId + this.appKey = token + this.cluster = cluster + + Log.d(TAG, "初始化参数: APP_ID=${appId}, APP_KEY长度=${token.length}, 集群区域=${cluster}") // 确保应用上下文已设置 if (applicationContext == null) { @@ -82,7 +87,7 @@ class SpeechRecognitionHelper : SpeechEngine.SpeechListener { mSpeechEngine?.setOptionString( mSpeechEngineHandler, SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, - SpeechEngineDefines.LOG_LEVEL_WARN + SpeechEngineDefines.LOG_LEVEL_DEBUG // 设置为DEBUG级别以获取更多日志信息 ) // 设置用户ID (必需) @@ -99,18 +104,18 @@ class SpeechRecognitionHelper : SpeechEngine.SpeechListener { "deep_voice_device" ) - // 设置授权信息 + // 设置授权信息 - APP_ID mSpeechEngine?.setOptionString( mSpeechEngineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, - subscriptionKey + appId ) - // 修改Token格式,不再使用"Bearer;"前缀 + // 设置授权信息 - APP_TOKEN (需要添加Bearer;前缀) mSpeechEngine?.setOptionString( mSpeechEngineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, - serviceRegion // 直接使用serviceRegion作为token + "Bearer;$token" // 添加Bearer;前缀 ) // 设置网络配置 @@ -119,15 +124,19 @@ class SpeechRecognitionHelper : SpeechEngine.SpeechListener { SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING, "wss://openspeech.bytedance.com" ) + + // 使用标准API路径 mSpeechEngine?.setOptionString( mSpeechEngineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING, "/api/v2/asr" ) + + // 设置集群区域 mSpeechEngine?.setOptionString( mSpeechEngineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_CLUSTER_STRING, - serviceRegion + cluster ) // 设置音频来源为内置录音机 @@ -170,6 +179,13 @@ class SpeechRecognitionHelper : SpeechEngine.SpeechListener { SpeechEngineDefines.ASR_RESULT_TYPE_FULL ) + // 记录所有配置参数,便于调试 + Log.d(TAG, "配置参数汇总:") + Log.d(TAG, "APP_ID: $appId") + Log.d(TAG, "APP_TOKEN: Bearer;${token.take(5)}***") + Log.d(TAG, "CLUSTER: $cluster") + Log.d(TAG, "API_PATH: /api/v2/asr") + // 初始化引擎 val ret = mSpeechEngine?.initEngine(mSpeechEngineHandler) ?: -1 if (ret != SpeechEngineDefines.ERR_NO_ERROR) { diff --git a/android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt index f66ea7447..7e6a90a13 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt @@ -13,12 +13,12 @@ import java.util.concurrent.TimeUnit * * 该类封装了火山语音SDK的TTS功能,提供简单的接口供Flutter调用 */ -class VolcanoTtsHelper : SpeechEngine.SpeechListener { +class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener { private val TAG = "VolcanoTtsHelper" private var mSpeechEngine: SpeechEngine? = null private var mSpeechEngineHandler: Long = -1 private var isInitialized = false - private var applicationContext: Context? = null + private var applicationContext: Context? = context.applicationContext // 同步合成相关变量 private var mSynthesisLatch: CountDownLatch? = null @@ -43,6 +43,8 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener { return false } + Log.d(TAG, "初始化火山语音TTS引擎,APP_ID: ${appId.take(3)}***,Token长度: ${token.length},集群区域: $cluster") + // 准备环境 SpeechEngineGenerator.PrepareEnvironment(applicationContext, null) @@ -57,6 +59,7 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener { // 设置上下文 mSpeechEngine?.setContext(applicationContext) + Log.d(TAG, "成功设置应用上下文") // 设置引擎类型为TTS mSpeechEngine?.setOptionString( @@ -171,13 +174,6 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener { return false } } - - /** - * 设置应用上下文 - */ - fun setContext(context: Context) { - applicationContext = context.applicationContext - } /** * 合成文本为语音 @@ -188,12 +184,13 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener { */ fun synthesize(text: String, voiceType: String, callback: VolcanoTtsCallback) { if (!isInitialized) { + Log.e(TAG, "TTS引擎尚未初始化") callback.onError("TTS引擎尚未初始化") return } try { - Log.d(TAG, "开始合成文本: $text") + Log.d(TAG, "开始合成文本: $text,使用语音类型: $voiceType") // 保存回调以便在onMessage中使用 mCurrentCallback = callback @@ -237,11 +234,14 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener { // 启动引擎,在单次合成场景下,这会自动开始合成 val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - callback.onError("启动引擎失败: $ret") + val errorMsg = "启动引擎失败: $ret" + Log.e(TAG, errorMsg) + callback.onError(errorMsg) } } catch (e: Exception) { - Log.e(TAG, "语音合成异常: ${e.message}") - callback.onError("语音合成异常: ${e.message}") + val errorMsg = "语音合成异常: ${e.message}" + Log.e(TAG, errorMsg) + callback.onError(errorMsg) } } @@ -259,7 +259,7 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener { } try { - Log.d(TAG, "开始同步合成文本: $text") + Log.d(TAG, "开始同步合成文本: $text,使用语音类型: $voiceType") // 重置同步变量 mSynthesisLatch = CountDownLatch(1) @@ -270,12 +270,12 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener { mSpeechEngine?.setOptionString( mSpeechEngineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_ONLINE_STRING, - voiceType + "other" ) mSpeechEngine?.setOptionString( mSpeechEngineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING, - "common" + "$voiceType" ) // 设置文本 @@ -342,9 +342,9 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener { } mSpeechEngine = null isInitialized = false - Log.d(TAG, "火山语音TTS引擎已释放") + Log.d(TAG, "火山语音合成引擎已释放") } catch (e: Exception) { - Log.e(TAG, "释放火山语音TTS引擎失败: ${e.message}") + Log.e(TAG, "释放火山语音合成引擎失败: ${e.message}") } } diff --git a/lib/data/services/volcano_tts_service.dart b/lib/data/services/volcano_tts_service.dart index 5e473c757..425f42bf1 100644 --- a/lib/data/services/volcano_tts_service.dart +++ b/lib/data/services/volcano_tts_service.dart @@ -184,23 +184,24 @@ class VolcanoTtsService extends GetxService { // 性能优化参数 final int _preloadCount = 2; // 预加载片段数量 - final int _maxRetryAttempts = 3; // 最大重试次数 + final int _maxRetryAttempts = 1; // 不进行重试 // 音频质量参数 final double _defaultVolume = 1.0; final double _defaultSpeed = 1.0; VolcanoTtsService() : - // 只使用统一的环境变量 - _appId = dotenv.env['VOLCANO_APP_ID'] ?? '', - _token = dotenv.env['VOLCANO_APP_KEY'] ?? '', - _cluster = dotenv.env['VOLCANO_CLUSTER'] ?? '', - _voiceType = dotenv.env['VOLCANO_VOICE_TYPE'] ?? '' { + // 使用新的环境变量命名 + _appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '', + _token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '', + _cluster = dotenv.env['VOLCANO_TTS_CLUSTER'] ?? '', + _voiceType = dotenv.env['VOLCANO_TTS_VOICE_TYPE'] ?? '' { if (_appId.isEmpty || _token.isEmpty || _cluster.isEmpty) { - throw VolcanoTtsException('火山语音配置信息不完整,请检查环境变量 VOLCANO_APP_ID, VOLCANO_APP_KEY 和 VOLCANO_CLUSTER'); + throw VolcanoTtsException('火山语音配置信息不完整,请检查环境变量 VOLCANO_TTS_APP_ID, VOLCANO_TTS_APP_KEY 和 VOLCANO_TTS_CLUSTER'); } } + @override Future onInit() async { super.onInit(); @@ -465,159 +466,67 @@ class VolcanoTtsService extends GetxService { for (final segment in segments) { print('获取音频片段: $segment'); - // 添加重试逻辑 - Uint8List? audioData; - int retryCount = 0; - - while (audioData == null && retryCount < _maxRetryAttempts) { - try { - // 使用提供的语音类型或默认语音类型 - audioData = await _synthesize(segment, voiceType: voiceType); - if (audioData == null && retryCount < _maxRetryAttempts - 1) { - print('合成返回空数据,尝试重试 (${retryCount + 1}/$_maxRetryAttempts)'); - await Future.delayed(Duration(milliseconds: 200 * (retryCount + 1))); - } - } catch (e) { - print('合成失败,尝试重试 (${retryCount + 1}/$_maxRetryAttempts): $e'); - if (retryCount < _maxRetryAttempts - 1) { - await Future.delayed(Duration(milliseconds: 200 * (retryCount + 1))); - } else { - rethrow; - } - } - retryCount++; - } - - if (audioData != null) { - print('成功获取音频数据: ${audioData.length} bytes'); - try { - final audioSource = BytesAudioSource(audioData); - await _playlist.add(audioSource); - print('音频片段已添加到播放列表'); - - // 如果是第一个片段且播放器未在播放,开始播放 - if (!_audioPlayer.playing && _playlist.length == 1) { - // 确保音频会话处于激活状态 - await _audioSession?.setActive(true); - - // 设置播放参数 - await _audioPlayer.setVolume(_defaultVolume); - await _audioPlayer.seek(Duration.zero, index: 0); - - // 开始播放 - await _audioPlayer.play(); - - // 重置连续错误计数 - _consecutiveErrorCount = 0; + try { + // 使用提供的语音类型或默认语音类型,只尝试一次 + final audioData = await _synthesize(segment, voiceType: voiceType); + + if (audioData != null) { + print('成功获取音频数据: ${audioData.length} bytes'); + try { + final audioSource = BytesAudioSource(audioData); + await _playlist.add(audioSource); + print('音频片段已添加到播放列表'); + + // 如果是第一个片段且播放器未在播放,开始播放 + if (!_audioPlayer.playing && _playlist.length == 1) { + print('开始播放'); + await _audioPlayer.play(); + } + } catch (e) { + print('添加音频到播放列表失败: $e'); } - } catch (e) { - print('添加音频源失败: $e'); - throw VolcanoTtsException('添加音频源失败', e); + } else { + print('获取音频失败: 合成返回空数据'); } - } else { - print('无法获取音频数据,跳过此片段'); - } - } - - // 如果队列还有内容,继续获取下一批 - if (_sentenceQueue.isNotEmpty) { - await Future.delayed(const Duration(milliseconds: 50)); // 减少延迟以提高响应速度 - await _fetchNextSegment(voiceType: voiceType); - } else { - _isFetching = false; - // 检查是否还有未处理的文本 - _extractSentences(); - if (_sentenceQueue.isNotEmpty) { - await _startPreloadIfNeeded(voiceType: voiceType); + } catch (e) { + print('获取音频失败: $e'); + // 不重试,继续处理下一个片段 } } - } catch (e, stackTrace) { - print('获取音频失败: $e'); - print('Stack trace: $stackTrace'); + } catch (e) { + print('预加载音频片段失败: $e'); + } finally { _isFetching = false; - - // 增加连续错误计数 - _consecutiveErrorCount++; - if (_consecutiveErrorCount >= _maxConsecutiveErrors) { - print('连续错误次数过多,尝试重新初始化播放器'); - await _reinitializePlayer(); - _consecutiveErrorCount = 0; - } } } - /// 使用原生SDK合成文本为语音 + /// 合成文本为音频数据 Future _synthesize(String text, {String? voiceType}) async { - if (!_isInitialized) { - try { - await _initializeTtsEngine(); - } catch (e) { - print('重新初始化TTS引擎失败: $e'); - throw VolcanoTtsException('重新初始化TTS引擎失败', e); - } + if (text.isEmpty) { + return null; } - // 定义基础语音类型列表,按优先级排序 - final fallbackVoiceTypes = [ - 'zh_male_qingse_common', - 'zh_female_qingse_common', - 'zh_male_M392_conversation_wvae_bigtts', - 'zh_female_F392_conversation_wvae_bigtts' - ]; - - // 确定要使用的语音类型 + // 使用指定的语音类型,不使用任何回退 final effectiveVoiceType = voiceType ?? _voiceType; - // 如果当前语音类型不在基础列表中,将其添加到首位 - if (!fallbackVoiceTypes.contains(effectiveVoiceType)) { - fallbackVoiceTypes.insert(0, effectiveVoiceType); - } - - // 尝试使用不同的语音类型 - VolcanoTtsException? lastException; - - for (final vType in fallbackVoiceTypes) { - try { - // 调用原生方法合成语音 - final result = await _channel.invokeMethod('synthesizeSync', { - 'text': text, - 'voiceType': vType, - }); - - if (result != null && result.isNotEmpty) { - // 如果使用的是回退语音类型,记录日志 - if (vType != effectiveVoiceType) { - print('使用回退语音类型成功: $vType (原始类型: $effectiveVoiceType)'); - } - return result; - } - } catch (e) { - // 检查是否是资源授权错误 - final errorMsg = e.toString().toLowerCase(); - final isAuthError = errorMsg.contains('resource not granted') || - errorMsg.contains('403') || - errorMsg.contains('授权') || - errorMsg.contains('3001'); - - if (isAuthError) { - print('语音类型 $vType 授权错误,尝试下一个语音类型'); - lastException = VolcanoTtsException('语音类型 $vType 授权错误', e); - continue; // 尝试下一个语音类型 - } else { - // 如果不是授权错误,直接抛出 - print('调用原生合成方法失败: $e'); - throw VolcanoTtsException('调用原生合成方法失败', e); - } + try { + // 调用原生方法合成语音 + final result = await _channel.invokeMethod('synthesizeSync', { + 'text': text, + 'voiceType': effectiveVoiceType, + }); + + if (result != null && result.isNotEmpty) { + return result; } + + // 如果没有结果,返回null + return null; + } catch (e) { + // 不处理授权错误,直接抛出所有错误 + print('调用原生合成方法失败: $e'); + throw VolcanoTtsException('调用原生合成方法失败', e); } - - // 如果所有语音类型都失败,抛出最后一个异常 - if (lastException != null) { - throw lastException; - } - - // 如果没有异常但也没有结果,返回null - return null; } /// 停止播放并清空队列 @@ -889,65 +798,4 @@ class VolcanoTtsService extends GetxService { return _voiceType; } - /// 获取可用的语音类型列表 - List getAvailableVoiceTypes() { - return [ - // 趣味方言 - 'zh_female_wanqudashu_moon_bigtts', // 湾区大叔 - 'zh_female_daimengchuanmei_moon_bigtts', // 呆萌川妹 - 'zh_male_guozhoudege_moon_bigtts', // 广州德哥 - 'zh_male_beijingxiaoye_moon_bigtts', // 北京小爷 - 'zh_male_haoyuxiaoge_moon_bigtts', // 浩宇小哥 - - // 通用场景 - 'zh_male_shaonianzixin_moon_bigtts', // 少年梓辛/Brayan - - // 角色扮演 - 'zh_female_meilinvyou_moon_bigtts', // 魅力女友 - 'zh_male_shenyeboke_moon_bigtts', // 深夜播客 - 'zh_female_sajiaonvyou_moon_bigtts', // 柔美女友 - 'zh_female_yuanqinvyou_moon_bigtts', // 撒娇学妹 - - // 基础语音类型 - 'zh_male_qingse_common', // 基础男声 - 'zh_female_qingse_common', // 基础女声 - 'zh_male_M392_conversation_wvae_bigtts', // 高级男声 - 'zh_female_F392_conversation_wvae_bigtts', // 高级女声 - ]; - } - - /// 检查语音类型是否可能可用 - /// 注意:此方法只是基于已知的基础语音类型进行判断,不保证实际可用性 - bool isVoiceTypeLikelyAvailable(String voiceType) { - // 基础语音类型,这些通常是免费可用的 - final basicVoiceTypes = [ - 'zh_male_qingse_common', - 'zh_female_qingse_common', - ]; - - // 如果是基础语音类型,则很可能可用 - if (basicVoiceTypes.contains(voiceType)) { - return true; - } - - // 其他语音类型可能需要授权 - final availableTypes = getAvailableVoiceTypes(); - return availableTypes.contains(voiceType); - } - - /// 设置语音类型 - /// 注意:由于_voiceType是final的,此方法不会实际修改成员变量 - /// 但可以在speak方法中使用传入的voiceType参数 - void setVoiceType(String voiceType) { - // 记录请求的语音类型变更 - print('请求设置语音类型: $voiceType (当前: $_voiceType)'); - - // 检查请求的语音类型是否可能可用 - if (!isVoiceTypeLikelyAvailable(voiceType)) { - print('警告: 请求的语音类型 $voiceType 可能不可用,建议使用基础语音类型'); - } - - // 这里不修改_voiceType成员变量,因为它是final的 - // 实际的语音类型设置是在synthesize方法中使用的 - } } \ No newline at end of file diff --git a/lib/data/services/volcano_voice_recognition_service.dart b/lib/data/services/volcano_voice_recognition_service.dart index 0fd414f52..ddd697883 100644 --- a/lib/data/services/volcano_voice_recognition_service.dart +++ b/lib/data/services/volcano_voice_recognition_service.dart @@ -31,54 +31,65 @@ class RecognitionEvent { /// /// 该服务提供了通过平台通道与 Android 上的火山语音识别 SDK 交互的接口 class VolcanoVoiceRecognitionService extends GetxService { - static const MethodChannel _channel = MethodChannel('com.example.deep_voice/speech_recognition'); + // 平台通道 + static const MethodChannel _channel = MethodChannel('com.example.deep_voice/volcano_asr'); static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/speech_recognition_events'); - bool _isInitialized = false; - late final String _subscriptionKey; - late final String _serviceRegion; + // 配置参数 + late final String _appId; + late final String _token; + late final String _cluster; // 集群区域 - // 连续识别相关 - bool _isContinuousRecognitionActive = false; - StreamController? _eventStreamController; - StreamSubscription? _eventSubscription; + // 状态变量 + final _isListening = false.obs; + final _isInitialized = false.obs; + final _errorMessage = ''.obs; + final _recognitionResults = [].obs; + + // 是否使用标准语音识别SDK + final bool _useStandardASR = true; // 使用标准语音识别SDK (API v2) + + // 添加识别结果流控制器 + final _recognitionStreamController = StreamController.broadcast(); - // 公开的事件流 - Stream? _recognitionStream; - Stream? get recognitionStream => _recognitionStream; + // 事件订阅 + StreamSubscription? _eventSubscription; // 最新的识别结果 final _latestRecognizedText = ''.obs; String get latestRecognizedText => _latestRecognizedText.value; - // 识别状态 - final isListening = false.obs; - - // 识别结果列表 - final RxList _recognitionResults = [].obs; - List get recognitionResults => _recognitionResults; - - // 错误信息 - final RxString _errorMessage = ''.obs; - String get errorMessage => _errorMessage.value; - - VolcanoVoiceRecognitionService() { - _loadConfig(); + /// 检查连续识别是否处于活动状态 + bool isContinuousRecognitionActive() { + return _isListening.value; } - /// 从环境变量加载配置 - void _loadConfig() { - // 只使用统一的APP_ID和APP_KEY - _subscriptionKey = dotenv.env['VOLCANO_APP_ID'] ?? ''; - _serviceRegion = dotenv.env['VOLCANO_APP_KEY'] ?? ''; + VolcanoVoiceRecognitionService() { + // 从环境变量获取配置 + _appId = dotenv.env['VOLCANO_ASR_APP_ID'] ?? ''; + _token = dotenv.env['VOLCANO_ASR_APP_KEY'] ?? ''; + // 从环境变量获取集群区域 + _cluster = dotenv.env['VOLCANO_ASR_CLUSTER'] ?? ''; - Logger.info('火山语音识别配置: APP_ID=${_subscriptionKey.isNotEmpty ? "已设置" : "未设置"}, APP_KEY=${_serviceRegion.isNotEmpty ? "已设置" : "未设置"}'); + Logger.info('火山语音识别配置: APP_ID=${_appId.isNotEmpty ? "已设置" : "未设置"}, APP_KEY=${_token.isNotEmpty ? "已设置" : "未设置"}'); + Logger.info('使用标准语音识别SDK: $_useStandardASR'); + Logger.info('集群区域: $_cluster'); - if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { - throw Exception('未找到火山语音服务配置。请在 .env 文件中设置 VOLCANO_APP_ID 和 VOLCANO_APP_KEY'); + if (_appId.isEmpty || _token.isEmpty) { + _errorMessage.value = '火山语音识别配置不完整,请检查环境变量'; + Logger.error(_errorMessage.value); } } + // 获取可观察状态 + RxBool get isListening => _isListening; + bool get isInitialized => _isInitialized.value; + String get errorMessage => _errorMessage.value; + List get recognitionResults => _recognitionResults; + + // 添加识别结果流getter + Stream get recognitionStream => _recognitionStreamController.stream; + @override void onInit() { super.onInit(); @@ -86,119 +97,104 @@ class VolcanoVoiceRecognitionService extends GetxService { } /// 设置方法通道处理器 - void _setupMethodCallHandler() { + Future _setupMethodCallHandler() async { _channel.setMethodCallHandler((call) async { switch (call.method) { case 'onRecognitionResult': final String result = call.arguments as String; - _handleRecognitionResult(result); + _onRecognitionResult(result); break; case 'onRecognitionError': final String error = call.arguments as String; - _handleRecognitionError(error); + _errorMessage.value = error; + Logger.error('识别错误: $error'); + _recognitionStreamController.add(RecognitionEvent( + type: RecognitionEventType.error, + error: error, + )); break; - case 'onRecognitionComplete': - _handleRecognitionComplete(); + case 'onListeningStateChanged': + final bool isListening = call.arguments as bool; + _isListening.value = isListening; + Logger.info('监听状态变化: $isListening'); break; } }); } /// 处理识别结果 - void _handleRecognitionResult(String result) { - Logger.debug('Recognition result: $result'); - _recognitionResults.add(result); - _latestRecognizedText.value = result; - - if (_eventStreamController != null) { - _eventStreamController!.add(RecognitionEvent( + void _onRecognitionResult(String result) { + if (result.isNotEmpty) { + _recognitionResults.add(result); + // 将结果发送到流 + _recognitionStreamController.add(RecognitionEvent( type: RecognitionEventType.finalResult, text: result, )); - } - } - - /// 处理识别错误 - void _handleRecognitionError(String error) { - Logger.error('Recognition error: $error'); - _errorMessage.value = error; - isListening.value = false; - - if (_eventStreamController != null) { - _eventStreamController!.add(RecognitionEvent( - type: RecognitionEventType.error, - text: '', - error: error, - )); - } - } - - /// 处理识别完成 - void _handleRecognitionComplete() { - Logger.debug('Recognition complete'); - isListening.value = false; - _isContinuousRecognitionActive = false; - - if (_eventStreamController != null) { - _eventStreamController!.add(RecognitionEvent( - type: RecognitionEventType.completed, - text: '', - )); + Logger.info('识别结果: $result'); } } /// 初始化语音识别引擎 Future initialize() async { - if (_isInitialized) return true; + if (_isInitialized.value) return true; + + if (_appId.isEmpty || _token.isEmpty) { + _errorMessage.value = '火山语音识别配置不完整,请检查环境变量'; + Logger.error(_errorMessage.value); + return false; + } try { - Logger.info('开始初始化火山语音识别服务,APP_ID: ${_subscriptionKey.substring(0, math.min(3, _subscriptionKey.length))}***,APP_KEY: ${_serviceRegion.length > 10 ? "${_serviceRegion.substring(0, 5)}..." : _serviceRegion}'); + _errorMessage.value = ''; + Logger.info('开始初始化火山语音识别服务,APP_ID: ${_appId.substring(0, math.min(3, _appId.length))}***,APP_KEY: ${_token.length > 10 ? "${_token.substring(0, 5)}..." : _token}'); - final bool result = await _channel.invokeMethod('initialize', { - 'subscriptionKey': _subscriptionKey, - 'serviceRegion': _serviceRegion, - }); + Logger.info('使用标准语音识别SDK配置 (API v2)'); + Logger.info('集群区域: $_cluster'); - _isInitialized = result; - Logger.info('火山语音识别服务初始化${result ? '成功' : '失败'}'); - return result; - } on PlatformException catch (e) { - // 检查是否是资源授权错误 - if (e.code == 'INITIALIZATION_ERROR' && - (e.message?.contains('资源授权错误') == true || - e.message?.contains('requested resource not granted') == true || - e.message?.contains('requested grant not found') == true)) { - Logger.error('火山语音识别初始化失败: 资源授权错误', e, StackTrace.current); - Logger.info('请检查以下几点:'); - Logger.info('1. 确保您的火山引擎账户已开通语音识别服务'); + final Map params = { + 'appId': _appId, + 'token': _token, + 'cluster': _cluster, + }; + + final bool result = await _channel.invokeMethod('initialize', params); + + if (result) { + _isInitialized.value = true; + Logger.info('火山语音识别服务初始化成功'); + } else { + _errorMessage.value = '初始化失败,请检查配置'; + Logger.error('火山语音识别服务初始化失败'); + Logger.info('请检查以下可能的问题:'); + Logger.info('1. 确保您的APP_ID和APP_KEY正确'); Logger.info('2. 确保您的应用ID和密钥正确且有效'); Logger.info('3. 确保您的应用已被授权使用语音识别服务'); - _isInitialized = false; - throw PlatformException( - code: 'RESOURCE_AUTHORIZATION_ERROR', - message: '语音识别服务授权失败: 请确保应用已开通语音识别服务并且密钥有效', - details: e.message - ); - } else { - Logger.error('火山语音识别初始化失败: ${e.message}', e, StackTrace.current); - _isInitialized = false; - throw e; } + + return result; } catch (e) { - Logger.error('火山语音识别初始化发生未知错误', e, StackTrace.current); - _isInitialized = false; - throw Exception('初始化火山语音识别服务失败: $e'); + _errorMessage.value = '初始化异常: $e'; + Logger.error('初始化火山语音识别服务异常: $e'); + Logger.info('请检查以下可能的问题:'); + Logger.info('1. 确保您的网络连接正常'); + Logger.info('2. 确保集群区域设置正确 (当前: $_cluster)'); + Logger.info('3. 确保请求参数格式正确'); + Logger.info('4. 确保Token格式正确,需要添加Bearer;前缀'); + + _isInitialized.value = false; + return false; } } /// 开始一次性识别 Future startOneTimeRecognition() async { - if (isListening.value) { + if (_isListening.value) { Logger.warning('已经在进行语音识别,请先停止当前识别'); return false; } - if (!_isInitialized) { + if (!_isInitialized.value) { try { final bool initialized = await initialize(); if (!initialized) { @@ -218,10 +214,10 @@ class VolcanoVoiceRecognitionService extends GetxService { _recognitionResults.clear(); final bool result = await _channel.invokeMethod('startOneTimeRecognition'); - isListening.value = result; + _isListening.value = result; if (result) { - _eventStreamController?.add(RecognitionEvent( + _recognitionStreamController.add(RecognitionEvent( type: RecognitionEventType.started, text: '', )); @@ -237,12 +233,12 @@ class VolcanoVoiceRecognitionService extends GetxService { /// 开始连续识别 Future startContinuousRecognition() async { - if (isListening.value) { + if (_isListening.value) { Logger.warning('已经在进行语音识别,请先停止当前识别'); return false; } - if (!_isInitialized) { + if (!_isInitialized.value) { try { final bool initialized = await initialize(); if (!initialized) { @@ -261,23 +257,23 @@ class VolcanoVoiceRecognitionService extends GetxService { _errorMessage.value = ''; _recognitionResults.clear(); - // 创建事件流控制器 - _eventStreamController = StreamController.broadcast(); - _recognitionStream = _eventStreamController?.stream; - // 设置事件监听 _eventSubscription = _eventChannel .receiveBroadcastStream() .listen(_handleNativeEvent, onError: (error) { - _handleRecognitionError(error.toString()); + _errorMessage.value = error.toString(); + _isListening.value = false; + _recognitionStreamController.add(RecognitionEvent( + type: RecognitionEventType.error, + error: error.toString(), + )); }); final bool result = await _channel.invokeMethod('startContinuousRecognition'); - isListening.value = result; - _isContinuousRecognitionActive = result; + _isListening.value = result; if (result) { - _eventStreamController?.add(RecognitionEvent( + _recognitionStreamController.add(RecognitionEvent( type: RecognitionEventType.started, text: '', )); @@ -302,7 +298,7 @@ class VolcanoVoiceRecognitionService extends GetxService { switch (eventType) { case 'recognizing': final String text = eventMap['text'] as String? ?? ''; - _eventStreamController?.add(RecognitionEvent( + _recognitionStreamController.add(RecognitionEvent( type: RecognitionEventType.recognizing, text: text, )); @@ -311,14 +307,19 @@ class VolcanoVoiceRecognitionService extends GetxService { final String text = eventMap['text'] as String? ?? ''; _latestRecognizedText.value = text; _recognitionResults.add(text); - _eventStreamController?.add(RecognitionEvent( + _recognitionStreamController.add(RecognitionEvent( type: RecognitionEventType.finalResult, text: text, )); break; case 'error': final String error = eventMap['error'] as String? ?? '未知错误'; - _handleRecognitionError(error); + _errorMessage.value = error; + _isListening.value = false; + _recognitionStreamController.add(RecognitionEvent( + type: RecognitionEventType.error, + error: error, + )); break; } } @@ -327,22 +328,18 @@ class VolcanoVoiceRecognitionService extends GetxService { void _cleanupEventStream() { _eventSubscription?.cancel(); _eventSubscription = null; - _eventStreamController?.close(); - _eventStreamController = null; - _recognitionStream = null; } /// 停止识别 Future stopRecognition() async { - if (!isListening.value) { + if (!_isListening.value) { Logger.warning('当前没有进行语音识别'); return false; } try { final bool result = await _channel.invokeMethod('stopRecognition'); - isListening.value = !result; - _isContinuousRecognitionActive = !result; + _isListening.value = !result; if (result) { _cleanupEventStream(); @@ -356,15 +353,10 @@ class VolcanoVoiceRecognitionService extends GetxService { } } - /// 检查连续识别是否活跃 - bool isContinuousRecognitionActive() { - return _isContinuousRecognitionActive; - } - /// 清理资源 Future dispose() async { try { - if (isListening.value) { + if (_isListening.value) { await stopRecognition(); } _cleanupEventStream(); @@ -375,7 +367,8 @@ class VolcanoVoiceRecognitionService extends GetxService { @override void onClose() { - dispose(); + _eventSubscription?.cancel(); + _recognitionStreamController.close(); super.onClose(); } } \ No newline at end of file diff --git a/lib/modules/test/controllers/tts_test_controller.dart b/lib/modules/test/controllers/tts_test_controller.dart index 15813f0ac..4ab2a76e2 100644 --- a/lib/modules/test/controllers/tts_test_controller.dart +++ b/lib/modules/test/controllers/tts_test_controller.dart @@ -74,10 +74,7 @@ class TtsTestController extends GetxController { // 获取当前语音类型 final currentVoiceType = selectedVoiceType.value; - // 检查语音类型是否可能可用 - if (!_ttsService.isVoiceTypeLikelyAvailable(currentVoiceType)) { - print('警告: 选择的语音类型 $currentVoiceType 可能不可用,但仍将尝试使用'); - } + // 直接使用选定的语音类型,不进行可用性检查 // 播放文本,传递选定的语音类型 await _ttsService.speak(text, voiceType: currentVoiceType); diff --git a/lib/tools/check_volcano_asr_config.dart b/lib/tools/check_volcano_asr_config.dart new file mode 100644 index 000000000..0c5f8028a --- /dev/null +++ b/lib/tools/check_volcano_asr_config.dart @@ -0,0 +1,259 @@ +import 'dart:io'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +/// 检查火山语音识别服务配置 +/// +/// 该工具用于验证火山语音识别服务的配置是否正确,包括: +/// 1. 检查环境变量是否设置 +/// 2. 检查网络连接 +/// 3. 检查认证是否有效 +void main() async { + // 加载环境变量 + await dotenv.load(); + + print('======== 火山语音识别服务配置检查 ========'); + + // 检查环境变量 + final appId = dotenv.env['VOLCANO_APP_ID']; + final appKey = dotenv.env['VOLCANO_APP_KEY']; + final cluster = dotenv.env['VOLCANO_CLUSTER']; + + print('\n1. 检查环境变量:'); + + if (appId == null || appId.isEmpty) { + print('❌ VOLCANO_APP_ID 未设置'); + } else { + print('✅ VOLCANO_APP_ID: ${appId.substring(0, 3)}***${appId.substring(appId.length - 3)} (长度: ${appId.length})'); + } + + if (appKey == null || appKey.isEmpty) { + print('❌ VOLCANO_APP_KEY 未设置'); + } else { + print('✅ VOLCANO_APP_KEY: ${appKey.substring(0, 3)}***${appKey.substring(appKey.length - 3)} (长度: ${appKey.length})'); + } + + if (cluster == null || cluster.isEmpty) { + print('❌ VOLCANO_CLUSTER 未设置,将使用默认值 cn-beijing'); + } else { + print('✅ VOLCANO_CLUSTER: $cluster'); + } + + // 使用默认值 + final effectiveCluster = cluster ?? 'cn-beijing'; + + if (appId == null || appId.isEmpty || appKey == null || appKey.isEmpty) { + print('\n❌ 环境变量配置不完整,请检查 .env 文件'); + exit(1); + } + + // 检查网络连接 + print('\n2. 检查网络连接:'); + + try { + final result = await InternetAddress.lookup('openspeech.bytedance.com'); + if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { + print('✅ 网络连接正常,可以访问 openspeech.bytedance.com'); + } else { + print('❌ 无法连接到 openspeech.bytedance.com'); + } + } catch (e) { + print('❌ 网络连接异常: $e'); + } + + // 检查认证是否有效 + print('\n3. 检查认证有效性:'); + + http.Response? authResponse; + + try { + // 构建请求URL - 更新为大模型流式识别API路径 + final url = 'https://openspeech.bytedance.com/api/v3/sauc/bigmodel'; + + // 构建请求头 - 不再添加Bearer前缀 + final headers = { + 'Content-Type': 'application/json', + 'Authorization': appKey, // 不再添加Bearer前缀 + }; + + // 构建请求体 - 添加resourceId参数 + final body = jsonEncode({ + 'app_id': appId, + 'cluster': effectiveCluster, + 'resource_id': appId, // 资源ID暂时使用与APP_ID相同的值 + 'ping': true, // 只是ping服务,不进行实际识别 + }); + + print('正在发送测试请求...'); + print('请求URL: $url'); + print('请求头: Authorization=${appKey.substring(0, 3)}***'); + print('集群区域: $effectiveCluster'); + + // 发送请求 + authResponse = await http.post( + Uri.parse(url), + headers: headers, + body: body, + ).timeout(const Duration(seconds: 5)); + + // 检查响应 + if (authResponse.statusCode == 200) { + print('✅ 认证有效,服务响应正常'); + print('响应内容: ${authResponse.body}'); + } else { + print('❌ 认证无效或服务异常,状态码: ${authResponse.statusCode}'); + print('错误信息: ${authResponse.body}'); + + // 解析错误信息 + try { + final errorJson = jsonDecode(authResponse.body); + final errorCode = errorJson['code']; + final errorMsg = errorJson['message']; + + if (errorCode == 401) { + print('\n认证错误,可能的原因:'); + print('1. APP_KEY 格式不正确'); + print('2. APP_ID 与 APP_KEY 不匹配'); + print('3. 账户未开通大模型流式语音识别服务或服务已过期'); + print('4. 资源ID不正确或未授权'); + } else if (errorCode == 400) { + print('\nWebSocket握手错误,可能的原因:'); + print('1. 集群区域设置不正确 (当前: $effectiveCluster)'); + print('2. 请求参数格式不正确'); + print('3. 尝试使用不同的集群区域,如 cn-shanghai 或 cn-guangzhou'); + } else { + print('\n未知错误:'); + print('错误码: $errorCode'); + print('错误信息: $errorMsg'); + } + } catch (e) { + print('\n解析错误信息失败: $e'); + + if (authResponse.statusCode == 400) { + print('\nWebSocket握手错误,可能的原因:'); + print('1. 集群区域设置不正确 (当前: $effectiveCluster)'); + print('2. 请求参数格式不正确'); + print('3. 尝试使用不同的集群区域,如 cn-shanghai 或 cn-guangzhou'); + } + } + } + } catch (e) { + print('❌ 请求异常: $e'); + } + + print('\n======== 检查完成 ========'); + print('\n提示: 如果您使用的是大模型流式识别SDK,请确保:'); + print('1. 使用了正确的API路径: /api/v3/sauc/bigmodel'); + print('2. 不要在Token前添加Bearer前缀'); + print('3. 设置了正确的资源ID'); + print('4. 设置了协议类型为PROTOCOL_TYPE_SEED'); + print('5. 设置了正确的集群区域 (当前: $effectiveCluster)'); + + // 尝试其他集群区域 + if (authResponse != null && authResponse.statusCode == 400) { + print('\n尝试其他集群区域:'); + final alternativeClusters = [ + 'cn-shanghai', + 'cn-guangzhou', + 'cn-hongkong', + 'ap-singapore', + 'us-east-1', + 'us-west-1' + ]; + + bool foundWorkingCluster = false; + + for (final altCluster in alternativeClusters) { + if (altCluster != effectiveCluster) { + print('\n尝试集群区域: $altCluster'); + final success = await _testCluster(appId, appKey, altCluster); + if (success) { + foundWorkingCluster = true; + print('\n✅ 找到可用的集群区域: $altCluster'); + print('建议在 .env 文件中设置 VOLCANO_CLUSTER=$altCluster'); + + // 尝试更新.env文件 + try { + await _updateEnvFile(altCluster); + } catch (e) { + print('无法自动更新.env文件: $e'); + } + + break; + } + } + } + + if (!foundWorkingCluster) { + print('\n❌ 所有集群区域测试均失败'); + print('请联系火山引擎技术支持获取正确的集群区域'); + } + } +} + +/// 测试不同的集群区域 +Future _testCluster(String appId, String appKey, String cluster) async { + try { + final url = 'https://openspeech.bytedance.com/api/v3/sauc/bigmodel'; + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': appKey, + }; + + final body = jsonEncode({ + 'app_id': appId, + 'cluster': cluster, + 'resource_id': appId, + 'ping': true, + }); + + final response = await http.post( + Uri.parse(url), + headers: headers, + body: body, + ).timeout(const Duration(seconds: 5)); + + if (response.statusCode == 200) { + print('✅ 集群区域 $cluster 可用,认证有效'); + return true; + } else { + print('❌ 集群区域 $cluster 不可用,状态码: ${response.statusCode}'); + return false; + } + } catch (e) { + print('❌ 测试集群区域 $cluster 时出错: $e'); + return false; + } +} + +/// 尝试更新.env文件 +Future _updateEnvFile(String newCluster) async { + try { + final file = File('.env'); + if (!await file.exists()) { + print('❌ .env文件不存在,无法自动更新'); + return; + } + + String content = await file.readAsString(); + + // 检查是否已有VOLCANO_CLUSTER + final clusterRegex = RegExp(r'VOLCANO_CLUSTER=.*'); + if (clusterRegex.hasMatch(content)) { + // 替换现有的VOLCANO_CLUSTER + content = content.replaceAll(clusterRegex, 'VOLCANO_CLUSTER=$newCluster'); + } else { + // 添加新的VOLCANO_CLUSTER + content += '\nVOLCANO_CLUSTER=$newCluster'; + } + + // 写入文件 + await file.writeAsString(content); + print('✅ 已自动更新.env文件中的VOLCANO_CLUSTER=$newCluster'); + } catch (e) { + print('❌ 更新.env文件失败: $e'); + throw e; + } +} \ No newline at end of file