Browse Source

add

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
6be8de9596
  1. 195
      VOLCANO_VOICE_README.md
  2. 23
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  3. 46
      android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt
  4. 36
      android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt
  5. 256
      lib/data/services/volcano_tts_service.dart
  6. 263
      lib/data/services/volcano_voice_recognition_service.dart
  7. 5
      lib/modules/test/controllers/tts_test_controller.dart
  8. 259
      lib/tools/check_volcano_asr_config.dart

195
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`)

23
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 SPEECH_RECOGNITION_EVENT_CHANNEL = "com.example.deep_voice/speech_recognition_events"
private val VOLCANO_TTS_CHANNEL = "com.example.deep_voice/volcano_tts" private val VOLCANO_TTS_CHANNEL = "com.example.deep_voice/volcano_tts"
private val TAG = "MainActivity" private val TAG = "MainActivity"
private val speechHelper = SpeechRecognitionHelper() private lateinit var speechHelper: SpeechRecognitionHelper
private val volcanoTtsHelper = VolcanoTtsHelper() private lateinit var volcanoTtsHelper: VolcanoTtsHelper
private var eventSink: EventChannel.EventSink? = null private var eventSink: EventChannel.EventSink? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// 设置火山语音合成Helper的上下文 // Initialize speechHelper with context
volcanoTtsHelper.setContext(applicationContext) speechHelper = SpeechRecognitionHelper(applicationContext)
// 设置火山语音识别Helper的上下文 // Initialize volcanoTtsHelper with context
speechHelper.setContext(applicationContext) volcanoTtsHelper = VolcanoTtsHelper(applicationContext)
} }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
@ -40,6 +40,8 @@ class MainActivity: AudioServiceActivity() {
"initialize" -> { "initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") val subscriptionKey = call.argument<String>("subscriptionKey")
val serviceRegion = call.argument<String>("serviceRegion") val serviceRegion = call.argument<String>("serviceRegion")
val resourceId = call.argument<String>("resourceId")
val cluster = call.argument<String>("cluster") ?: "cn-beijing"
if (subscriptionKey == null || serviceRegion == null) { if (subscriptionKey == null || serviceRegion == null) {
result.error("INVALID_ARGUMENTS", "subscriptionKey and serviceRegion are required", null) result.error("INVALID_ARGUMENTS", "subscriptionKey and serviceRegion are required", null)
@ -47,8 +49,13 @@ class MainActivity: AudioServiceActivity() {
} }
try { try {
Log.d(TAG, "初始化语音识别服务,APP_ID: ${subscriptionKey.take(3)}***,APP_KEY: ${serviceRegion.take(5)}...") 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.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 "失败"}") Log.d(TAG, "语音识别服务初始化${if (success) "成功" else "失败"}")
result.success(success) result.success(success)
} catch (e: Exception) { } catch (e: Exception) {

46
android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt

@ -15,16 +15,17 @@ import java.util.concurrent.TimeUnit
* *
* 该类封装了火山语音SDK的语音识别功能,提供简单的接口供Flutter调用 * 该类封装了火山语音SDK的语音识别功能,提供简单的接口供Flutter调用
*/ */
class SpeechRecognitionHelper : SpeechEngine.SpeechListener { class SpeechRecognitionHelper(context: Context) : SpeechEngine.SpeechListener {
private val TAG = "SpeechRecognitionHelper" private val TAG = "SpeechRecognitionHelper"
private var mSpeechEngine: SpeechEngine? = null private var mSpeechEngine: SpeechEngine? = null
private var mSpeechEngineHandler: Long = -1 private var mSpeechEngineHandler: Long = -1
private var isInitialized = false private var isInitialized = false
private var applicationContext: Context? = null private var applicationContext: Context? = context.applicationContext
// 存储应用ID和密钥,以便在错误处理中使用 // 存储应用ID和密钥,以便在错误处理中使用
private var appId: String = "" private var appId: String = ""
private var appKey: String = "" private var appKey: String = ""
private var cluster: String = "" // 移除默认值
// 同步识别相关变量 // 同步识别相关变量
private var mRecognitionLatch: CountDownLatch? = null private var mRecognitionLatch: CountDownLatch? = null
@ -40,15 +41,19 @@ class SpeechRecognitionHelper : SpeechEngine.SpeechListener {
/** /**
* 初始化语音识别引擎 * 初始化语音识别引擎
* * @param appId 应用ID
* @param subscriptionKey 订阅密钥 * @param token 应用密钥/令牌
* @param serviceRegion 服务区域 * @param cluster 集群区域
* @return 初始化是否成功
*/ */
fun initialize(subscriptionKey: String, serviceRegion: String): Boolean { fun initialize(appId: String, token: String, cluster: String): Boolean {
try { try {
// 保存应用ID和密钥,以便在错误处理中使用 // 设置基本参数
this.appId = subscriptionKey this.appId = appId
this.appKey = serviceRegion this.appKey = token
this.cluster = cluster
Log.d(TAG, "初始化参数: APP_ID=${appId}, APP_KEY长度=${token.length}, 集群区域=${cluster}")
// 确保应用上下文已设置 // 确保应用上下文已设置
if (applicationContext == null) { if (applicationContext == null) {
@ -82,7 +87,7 @@ class SpeechRecognitionHelper : SpeechEngine.SpeechListener {
mSpeechEngine?.setOptionString( mSpeechEngine?.setOptionString(
mSpeechEngineHandler, mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING,
SpeechEngineDefines.LOG_LEVEL_WARN SpeechEngineDefines.LOG_LEVEL_DEBUG // 设置为DEBUG级别以获取更多日志信息
) )
// 设置用户ID (必需) // 设置用户ID (必需)
@ -99,18 +104,18 @@ class SpeechRecognitionHelper : SpeechEngine.SpeechListener {
"deep_voice_device" "deep_voice_device"
) )
// 设置授权信息 // 设置授权信息 - APP_ID
mSpeechEngine?.setOptionString( mSpeechEngine?.setOptionString(
mSpeechEngineHandler, mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING,
subscriptionKey appId
) )
// 修改Token格式,不再使用"Bearer;"前缀 // 设置授权信息 - APP_TOKEN (需要添加Bearer;前缀)
mSpeechEngine?.setOptionString( mSpeechEngine?.setOptionString(
mSpeechEngineHandler, mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, 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, SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING,
"wss://openspeech.bytedance.com" "wss://openspeech.bytedance.com"
) )
// 使用标准API路径
mSpeechEngine?.setOptionString( mSpeechEngine?.setOptionString(
mSpeechEngineHandler, mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING, SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING,
"/api/v2/asr" "/api/v2/asr"
) )
// 设置集群区域
mSpeechEngine?.setOptionString( mSpeechEngine?.setOptionString(
mSpeechEngineHandler, mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_CLUSTER_STRING, SpeechEngineDefines.PARAMS_KEY_ASR_CLUSTER_STRING,
serviceRegion cluster
) )
// 设置音频来源为内置录音机 // 设置音频来源为内置录音机
@ -170,6 +179,13 @@ class SpeechRecognitionHelper : SpeechEngine.SpeechListener {
SpeechEngineDefines.ASR_RESULT_TYPE_FULL 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 val ret = mSpeechEngine?.initEngine(mSpeechEngineHandler) ?: -1
if (ret != SpeechEngineDefines.ERR_NO_ERROR) { if (ret != SpeechEngineDefines.ERR_NO_ERROR) {

36
android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt

@ -13,12 +13,12 @@ import java.util.concurrent.TimeUnit
* *
* 该类封装了火山语音SDK的TTS功能,提供简单的接口供Flutter调用 * 该类封装了火山语音SDK的TTS功能,提供简单的接口供Flutter调用
*/ */
class VolcanoTtsHelper : SpeechEngine.SpeechListener { class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
private val TAG = "VolcanoTtsHelper" private val TAG = "VolcanoTtsHelper"
private var mSpeechEngine: SpeechEngine? = null private var mSpeechEngine: SpeechEngine? = null
private var mSpeechEngineHandler: Long = -1 private var mSpeechEngineHandler: Long = -1
private var isInitialized = false private var isInitialized = false
private var applicationContext: Context? = null private var applicationContext: Context? = context.applicationContext
// 同步合成相关变量 // 同步合成相关变量
private var mSynthesisLatch: CountDownLatch? = null private var mSynthesisLatch: CountDownLatch? = null
@ -43,6 +43,8 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener {
return false return false
} }
Log.d(TAG, "初始化火山语音TTS引擎,APP_ID: ${appId.take(3)}***,Token长度: ${token.length},集群区域: $cluster")
// 准备环境 // 准备环境
SpeechEngineGenerator.PrepareEnvironment(applicationContext, null) SpeechEngineGenerator.PrepareEnvironment(applicationContext, null)
@ -57,6 +59,7 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener {
// 设置上下文 // 设置上下文
mSpeechEngine?.setContext(applicationContext) mSpeechEngine?.setContext(applicationContext)
Log.d(TAG, "成功设置应用上下文")
// 设置引擎类型为TTS // 设置引擎类型为TTS
mSpeechEngine?.setOptionString( mSpeechEngine?.setOptionString(
@ -172,13 +175,6 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener {
} }
} }
/**
* 设置应用上下文
*/
fun setContext(context: Context) {
applicationContext = context.applicationContext
}
/** /**
* 合成文本为语音 * 合成文本为语音
* *
@ -188,12 +184,13 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener {
*/ */
fun synthesize(text: String, voiceType: String, callback: VolcanoTtsCallback) { fun synthesize(text: String, voiceType: String, callback: VolcanoTtsCallback) {
if (!isInitialized) { if (!isInitialized) {
Log.e(TAG, "TTS引擎尚未初始化")
callback.onError("TTS引擎尚未初始化") callback.onError("TTS引擎尚未初始化")
return return
} }
try { try {
Log.d(TAG, "开始合成文本: $text") Log.d(TAG, "开始合成文本: $text,使用语音类型: $voiceType")
// 保存回调以便在onMessage中使用 // 保存回调以便在onMessage中使用
mCurrentCallback = callback mCurrentCallback = callback
@ -237,11 +234,14 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener {
// 启动引擎,在单次合成场景下,这会自动开始合成 // 启动引擎,在单次合成场景下,这会自动开始合成
val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) { if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
callback.onError("启动引擎失败: $ret") val errorMsg = "启动引擎失败: $ret"
Log.e(TAG, errorMsg)
callback.onError(errorMsg)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "语音合成异常: ${e.message}") val errorMsg = "语音合成异常: ${e.message}"
callback.onError("语音合成异常: ${e.message}") Log.e(TAG, errorMsg)
callback.onError(errorMsg)
} }
} }
@ -259,7 +259,7 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener {
} }
try { try {
Log.d(TAG, "开始同步合成文本: $text") Log.d(TAG, "开始同步合成文本: $text,使用语音类型: $voiceType")
// 重置同步变量 // 重置同步变量
mSynthesisLatch = CountDownLatch(1) mSynthesisLatch = CountDownLatch(1)
@ -270,12 +270,12 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener {
mSpeechEngine?.setOptionString( mSpeechEngine?.setOptionString(
mSpeechEngineHandler, mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_ONLINE_STRING, SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_ONLINE_STRING,
voiceType "other"
) )
mSpeechEngine?.setOptionString( mSpeechEngine?.setOptionString(
mSpeechEngineHandler, mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING, SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING,
"common" "$voiceType"
) )
// 设置文本 // 设置文本
@ -342,9 +342,9 @@ class VolcanoTtsHelper : SpeechEngine.SpeechListener {
} }
mSpeechEngine = null mSpeechEngine = null
isInitialized = false isInitialized = false
Log.d(TAG, "火山语音TTS引擎已释放") Log.d(TAG, "火山语音合成引擎已释放")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "释放火山语音TTS引擎失败: ${e.message}") Log.e(TAG, "释放火山语音合成引擎失败: ${e.message}")
} }
} }

256
lib/data/services/volcano_tts_service.dart

@ -184,23 +184,24 @@ class VolcanoTtsService extends GetxService {
// 性能优化参数 // 性能优化参数
final int _preloadCount = 2; // 预加载片段数量 final int _preloadCount = 2; // 预加载片段数量
final int _maxRetryAttempts = 3; // 最大重试次数 final int _maxRetryAttempts = 1; // 不进行重试
// 音频质量参数 // 音频质量参数
final double _defaultVolume = 1.0; final double _defaultVolume = 1.0;
final double _defaultSpeed = 1.0; final double _defaultSpeed = 1.0;
VolcanoTtsService() : VolcanoTtsService() :
// 只使用统一的环境变量 // 使用新的环境变量命名
_appId = dotenv.env['VOLCANO_APP_ID'] ?? '', _appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '',
_token = dotenv.env['VOLCANO_APP_KEY'] ?? '', _token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '',
_cluster = dotenv.env['VOLCANO_CLUSTER'] ?? '', _cluster = dotenv.env['VOLCANO_TTS_CLUSTER'] ?? '',
_voiceType = dotenv.env['VOLCANO_VOICE_TYPE'] ?? '' { _voiceType = dotenv.env['VOLCANO_TTS_VOICE_TYPE'] ?? '' {
if (_appId.isEmpty || _token.isEmpty || _cluster.isEmpty) { 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 @override
Future<void> onInit() async { Future<void> onInit() async {
super.onInit(); super.onInit();
@ -465,159 +466,67 @@ class VolcanoTtsService extends GetxService {
for (final segment in segments) { for (final segment in segments) {
print('获取音频片段: $segment'); print('获取音频片段: $segment');
// 添加重试逻辑 try {
Uint8List? audioData; // 使用提供的语音类型或默认语音类型,只尝试一次
int retryCount = 0; final audioData = await _synthesize(segment, voiceType: voiceType);
while (audioData == null && retryCount < _maxRetryAttempts) { if (audioData != null) {
try { print('成功获取音频数据: ${audioData.length} bytes');
// 使用提供的语音类型或默认语音类型 try {
audioData = await _synthesize(segment, voiceType: voiceType); final audioSource = BytesAudioSource(audioData);
if (audioData == null && retryCount < _maxRetryAttempts - 1) { await _playlist.add(audioSource);
print('合成返回空数据,尝试重试 (${retryCount + 1}/$_maxRetryAttempts)'); print('音频片段已添加到播放列表');
await Future.delayed(Duration(milliseconds: 200 * (retryCount + 1)));
} // 如果是第一个片段且播放器未在播放,开始播放
} catch (e) { if (!_audioPlayer.playing && _playlist.length == 1) {
print('合成失败,尝试重试 (${retryCount + 1}/$_maxRetryAttempts): $e'); print('开始播放');
if (retryCount < _maxRetryAttempts - 1) { await _audioPlayer.play();
await Future.delayed(Duration(milliseconds: 200 * (retryCount + 1))); }
} else { } catch (e) {
rethrow; print('添加音频到播放列表失败: $e');
}
}
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;
} }
} catch (e) { } else {
print('添加音频源失败: $e'); print('获取音频失败: 合成返回空数据');
throw VolcanoTtsException('添加音频源失败', e);
} }
} else { } catch (e) {
print('无法获取音频数据,跳过此片段'); print('获取音频失败: $e');
} // 不重试,继续处理下一个片段
}
// 如果队列还有内容,继续获取下一批
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, stackTrace) { } catch (e) {
print('获取音频失败: $e'); print('预加载音频片段失败: $e');
print('Stack trace: $stackTrace'); } finally {
_isFetching = false; _isFetching = false;
// 增加连续错误计数
_consecutiveErrorCount++;
if (_consecutiveErrorCount >= _maxConsecutiveErrors) {
print('连续错误次数过多,尝试重新初始化播放器');
await _reinitializePlayer();
_consecutiveErrorCount = 0;
}
} }
} }
/// 使用原生SDK合成文本为语音 /// 合成文本为音频数据
Future<Uint8List?> _synthesize(String text, {String? voiceType}) async { Future<Uint8List?> _synthesize(String text, {String? voiceType}) async {
if (!_isInitialized) { if (text.isEmpty) {
try { return null;
await _initializeTtsEngine();
} catch (e) {
print('重新初始化TTS引擎失败: $e');
throw VolcanoTtsException('重新初始化TTS引擎失败', e);
}
} }
// 定义基础语音类型列表,按优先级排序 // 使用指定的语音类型,不使用任何回退
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; final effectiveVoiceType = voiceType ?? _voiceType;
// 如果当前语音类型不在基础列表中,将其添加到首位 try {
if (!fallbackVoiceTypes.contains(effectiveVoiceType)) { // 调用原生方法合成语音
fallbackVoiceTypes.insert(0, effectiveVoiceType); final result = await _channel.invokeMethod<Uint8List>('synthesizeSync', {
} 'text': text,
'voiceType': effectiveVoiceType,
// 尝试使用不同的语音类型 });
VolcanoTtsException? lastException;
for (final vType in fallbackVoiceTypes) { if (result != null && result.isNotEmpty) {
try { return result;
// 调用原生方法合成语音
final result = await _channel.invokeMethod<Uint8List>('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);
}
} }
}
// 如果所有语音类型都失败,抛出最后一个异常 // 如果没有结果,返回null
if (lastException != null) { return null;
throw lastException; } catch (e) {
// 不处理授权错误,直接抛出所有错误
print('调用原生合成方法失败: $e');
throw VolcanoTtsException('调用原生合成方法失败', e);
} }
// 如果没有异常但也没有结果,返回null
return null;
} }
/// 停止播放并清空队列 /// 停止播放并清空队列
@ -889,65 +798,4 @@ class VolcanoTtsService extends GetxService {
return _voiceType; return _voiceType;
} }
/// 获取可用的语音类型列表
List<String> 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方法中使用的
}
} }

263
lib/data/services/volcano_voice_recognition_service.dart

@ -31,53 +31,64 @@ class RecognitionEvent {
/// ///
/// 该服务提供了通过平台通道与 Android 上的火山语音识别 SDK 交互的接口 /// 该服务提供了通过平台通道与 Android 上的火山语音识别 SDK 交互的接口
class VolcanoVoiceRecognitionService extends GetxService { 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'); static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/speech_recognition_events');
bool _isInitialized = false; // 配置参数
late final String _subscriptionKey; late final String _appId;
late final String _serviceRegion; late final String _token;
late final String _cluster; // 集群区域
// 连续识别相关 // 状态变量
bool _isContinuousRecognitionActive = false; final _isListening = false.obs;
StreamController<RecognitionEvent>? _eventStreamController; final _isInitialized = false.obs;
StreamSubscription? _eventSubscription; final _errorMessage = ''.obs;
final _recognitionResults = <String>[].obs;
// 是否使用标准语音识别SDK
final bool _useStandardASR = true; // 使用标准语音识别SDK (API v2)
// 添加识别结果流控制器
final _recognitionStreamController = StreamController<RecognitionEvent>.broadcast();
// 公开的事件流 // 事件订阅
Stream<RecognitionEvent>? _recognitionStream; StreamSubscription? _eventSubscription;
Stream<RecognitionEvent>? get recognitionStream => _recognitionStream;
// 最新的识别结果 // 最新的识别结果
final _latestRecognizedText = ''.obs; final _latestRecognizedText = ''.obs;
String get latestRecognizedText => _latestRecognizedText.value; String get latestRecognizedText => _latestRecognizedText.value;
// 识别状态 /// 检查连续识别是否处于活动状态
final isListening = false.obs; bool isContinuousRecognitionActive() {
return _isListening.value;
// 识别结果列表 }
final RxList<String> _recognitionResults = <String>[].obs;
List<String> get recognitionResults => _recognitionResults;
// 错误信息
final RxString _errorMessage = ''.obs;
String get errorMessage => _errorMessage.value;
VolcanoVoiceRecognitionService() { VolcanoVoiceRecognitionService() {
_loadConfig(); // 从环境变量获取配置
_appId = dotenv.env['VOLCANO_ASR_APP_ID'] ?? '';
_token = dotenv.env['VOLCANO_ASR_APP_KEY'] ?? '';
// 从环境变量获取集群区域
_cluster = dotenv.env['VOLCANO_ASR_CLUSTER'] ?? '';
Logger.info('火山语音识别配置: APP_ID=${_appId.isNotEmpty ? "已设置" : "未设置"}, APP_KEY=${_token.isNotEmpty ? "已设置" : "未设置"}');
Logger.info('使用标准语音识别SDK: $_useStandardASR');
Logger.info('集群区域: $_cluster');
if (_appId.isEmpty || _token.isEmpty) {
_errorMessage.value = '火山语音识别配置不完整,请检查环境变量';
Logger.error(_errorMessage.value);
}
} }
/// 从环境变量加载配置 // 获取可观察状态
void _loadConfig() { RxBool get isListening => _isListening;
// 只使用统一的APP_ID和APP_KEY bool get isInitialized => _isInitialized.value;
_subscriptionKey = dotenv.env['VOLCANO_APP_ID'] ?? ''; String get errorMessage => _errorMessage.value;
_serviceRegion = dotenv.env['VOLCANO_APP_KEY'] ?? ''; List<String> get recognitionResults => _recognitionResults;
Logger.info('火山语音识别配置: APP_ID=${_subscriptionKey.isNotEmpty ? "已设置" : "未设置"}, APP_KEY=${_serviceRegion.isNotEmpty ? "已设置" : "未设置"}');
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { // 添加识别结果流getter
throw Exception('未找到火山语音服务配置。请在 .env 文件中设置 VOLCANO_APP_ID 和 VOLCANO_APP_KEY'); Stream<RecognitionEvent> get recognitionStream => _recognitionStreamController.stream;
}
}
@override @override
void onInit() { void onInit() {
@ -86,119 +97,104 @@ class VolcanoVoiceRecognitionService extends GetxService {
} }
/// 设置方法通道处理器 /// 设置方法通道处理器
void _setupMethodCallHandler() { Future<void> _setupMethodCallHandler() async {
_channel.setMethodCallHandler((call) async { _channel.setMethodCallHandler((call) async {
switch (call.method) { switch (call.method) {
case 'onRecognitionResult': case 'onRecognitionResult':
final String result = call.arguments as String; final String result = call.arguments as String;
_handleRecognitionResult(result); _onRecognitionResult(result);
break; break;
case 'onRecognitionError': case 'onRecognitionError':
final String error = call.arguments as String; final String error = call.arguments as String;
_handleRecognitionError(error); _errorMessage.value = error;
Logger.error('识别错误: $error');
_recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error,
));
break; break;
case 'onRecognitionComplete': case 'onListeningStateChanged':
_handleRecognitionComplete(); final bool isListening = call.arguments as bool;
_isListening.value = isListening;
Logger.info('监听状态变化: $isListening');
break; break;
} }
}); });
} }
/// 处理识别结果 /// 处理识别结果
void _handleRecognitionResult(String result) { void _onRecognitionResult(String result) {
Logger.debug('Recognition result: $result'); if (result.isNotEmpty) {
_recognitionResults.add(result); _recognitionResults.add(result);
_latestRecognizedText.value = result; // 将结果发送到流
_recognitionStreamController.add(RecognitionEvent(
if (_eventStreamController != null) {
_eventStreamController!.add(RecognitionEvent(
type: RecognitionEventType.finalResult, type: RecognitionEventType.finalResult,
text: result, text: result,
)); ));
} Logger.info('识别结果: $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: '',
));
} }
} }
/// 初始化语音识别引擎 /// 初始化语音识别引擎
Future<bool> initialize() async { Future<bool> 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 { 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', { Logger.info('使用标准语音识别SDK配置 (API v2)');
'subscriptionKey': _subscriptionKey, Logger.info('集群区域: $_cluster');
'serviceRegion': _serviceRegion,
});
_isInitialized = result; final Map<String, dynamic> params = {
Logger.info('火山语音识别服务初始化${result ? '成功' : '失败'}'); 'appId': _appId,
return result; 'token': _token,
} on PlatformException catch (e) { 'cluster': _cluster,
// 检查是否是资源授权错误 };
if (e.code == 'INITIALIZATION_ERROR' &&
(e.message?.contains('资源授权错误') == true || final bool result = await _channel.invokeMethod('initialize', params);
e.message?.contains('requested resource not granted') == true ||
e.message?.contains('requested grant not found') == true)) { if (result) {
Logger.error('火山语音识别初始化失败: 资源授权错误', e, StackTrace.current); _isInitialized.value = true;
Logger.info('请检查以下几点:'); Logger.info('火山语音识别服务初始化成功');
Logger.info('1. 确保您的火山引擎账户已开通语音识别服务'); } else {
_errorMessage.value = '初始化失败,请检查配置';
Logger.error('火山语音识别服务初始化失败');
Logger.info('请检查以下可能的问题:');
Logger.info('1. 确保您的APP_ID和APP_KEY正确');
Logger.info('2. 确保您的应用ID和密钥正确且有效'); Logger.info('2. 确保您的应用ID和密钥正确且有效');
Logger.info('3. 确保您的应用已被授权使用语音识别服务'); 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) { } catch (e) {
Logger.error('火山语音识别初始化发生未知错误', e, StackTrace.current); _errorMessage.value = '初始化异常: $e';
_isInitialized = false; Logger.error('初始化火山语音识别服务异常: $e');
throw Exception('初始化火山语音识别服务失败: $e'); Logger.info('请检查以下可能的问题:');
Logger.info('1. 确保您的网络连接正常');
Logger.info('2. 确保集群区域设置正确 (当前: $_cluster)');
Logger.info('3. 确保请求参数格式正确');
Logger.info('4. 确保Token格式正确,需要添加Bearer;前缀');
_isInitialized.value = false;
return false;
} }
} }
/// 开始一次性识别 /// 开始一次性识别
Future<bool> startOneTimeRecognition() async { Future<bool> startOneTimeRecognition() async {
if (isListening.value) { if (_isListening.value) {
Logger.warning('已经在进行语音识别,请先停止当前识别'); Logger.warning('已经在进行语音识别,请先停止当前识别');
return false; return false;
} }
if (!_isInitialized) { if (!_isInitialized.value) {
try { try {
final bool initialized = await initialize(); final bool initialized = await initialize();
if (!initialized) { if (!initialized) {
@ -218,10 +214,10 @@ class VolcanoVoiceRecognitionService extends GetxService {
_recognitionResults.clear(); _recognitionResults.clear();
final bool result = await _channel.invokeMethod('startOneTimeRecognition'); final bool result = await _channel.invokeMethod('startOneTimeRecognition');
isListening.value = result; _isListening.value = result;
if (result) { if (result) {
_eventStreamController?.add(RecognitionEvent( _recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.started, type: RecognitionEventType.started,
text: '', text: '',
)); ));
@ -237,12 +233,12 @@ class VolcanoVoiceRecognitionService extends GetxService {
/// 开始连续识别 /// 开始连续识别
Future<bool> startContinuousRecognition() async { Future<bool> startContinuousRecognition() async {
if (isListening.value) { if (_isListening.value) {
Logger.warning('已经在进行语音识别,请先停止当前识别'); Logger.warning('已经在进行语音识别,请先停止当前识别');
return false; return false;
} }
if (!_isInitialized) { if (!_isInitialized.value) {
try { try {
final bool initialized = await initialize(); final bool initialized = await initialize();
if (!initialized) { if (!initialized) {
@ -261,23 +257,23 @@ class VolcanoVoiceRecognitionService extends GetxService {
_errorMessage.value = ''; _errorMessage.value = '';
_recognitionResults.clear(); _recognitionResults.clear();
// 创建事件流控制器
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
_recognitionStream = _eventStreamController?.stream;
// 设置事件监听 // 设置事件监听
_eventSubscription = _eventChannel _eventSubscription = _eventChannel
.receiveBroadcastStream() .receiveBroadcastStream()
.listen(_handleNativeEvent, onError: (error) { .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'); final bool result = await _channel.invokeMethod('startContinuousRecognition');
isListening.value = result; _isListening.value = result;
_isContinuousRecognitionActive = result;
if (result) { if (result) {
_eventStreamController?.add(RecognitionEvent( _recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.started, type: RecognitionEventType.started,
text: '', text: '',
)); ));
@ -302,7 +298,7 @@ class VolcanoVoiceRecognitionService extends GetxService {
switch (eventType) { switch (eventType) {
case 'recognizing': case 'recognizing':
final String text = eventMap['text'] as String? ?? ''; final String text = eventMap['text'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent( _recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.recognizing, type: RecognitionEventType.recognizing,
text: text, text: text,
)); ));
@ -311,14 +307,19 @@ class VolcanoVoiceRecognitionService extends GetxService {
final String text = eventMap['text'] as String? ?? ''; final String text = eventMap['text'] as String? ?? '';
_latestRecognizedText.value = text; _latestRecognizedText.value = text;
_recognitionResults.add(text); _recognitionResults.add(text);
_eventStreamController?.add(RecognitionEvent( _recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.finalResult, type: RecognitionEventType.finalResult,
text: text, text: text,
)); ));
break; break;
case 'error': case 'error':
final String error = eventMap['error'] as String? ?? '未知错误'; final String error = eventMap['error'] as String? ?? '未知错误';
_handleRecognitionError(error); _errorMessage.value = error;
_isListening.value = false;
_recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error,
));
break; break;
} }
} }
@ -327,22 +328,18 @@ class VolcanoVoiceRecognitionService extends GetxService {
void _cleanupEventStream() { void _cleanupEventStream() {
_eventSubscription?.cancel(); _eventSubscription?.cancel();
_eventSubscription = null; _eventSubscription = null;
_eventStreamController?.close();
_eventStreamController = null;
_recognitionStream = null;
} }
/// 停止识别 /// 停止识别
Future<bool> stopRecognition() async { Future<bool> stopRecognition() async {
if (!isListening.value) { if (!_isListening.value) {
Logger.warning('当前没有进行语音识别'); Logger.warning('当前没有进行语音识别');
return false; return false;
} }
try { try {
final bool result = await _channel.invokeMethod('stopRecognition'); final bool result = await _channel.invokeMethod('stopRecognition');
isListening.value = !result; _isListening.value = !result;
_isContinuousRecognitionActive = !result;
if (result) { if (result) {
_cleanupEventStream(); _cleanupEventStream();
@ -356,15 +353,10 @@ class VolcanoVoiceRecognitionService extends GetxService {
} }
} }
/// 检查连续识别是否活跃
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
/// 清理资源 /// 清理资源
Future<void> dispose() async { Future<void> dispose() async {
try { try {
if (isListening.value) { if (_isListening.value) {
await stopRecognition(); await stopRecognition();
} }
_cleanupEventStream(); _cleanupEventStream();
@ -375,7 +367,8 @@ class VolcanoVoiceRecognitionService extends GetxService {
@override @override
void onClose() { void onClose() {
dispose(); _eventSubscription?.cancel();
_recognitionStreamController.close();
super.onClose(); super.onClose();
} }
} }

5
lib/modules/test/controllers/tts_test_controller.dart

@ -74,10 +74,7 @@ class TtsTestController extends GetxController {
// 获取当前语音类型 // 获取当前语音类型
final currentVoiceType = selectedVoiceType.value; final currentVoiceType = selectedVoiceType.value;
// 检查语音类型是否可能可用 // 直接使用选定的语音类型,不进行可用性检查
if (!_ttsService.isVoiceTypeLikelyAvailable(currentVoiceType)) {
print('警告: 选择的语音类型 $currentVoiceType 可能不可用,但仍将尝试使用');
}
// 播放文本,传递选定的语音类型 // 播放文本,传递选定的语音类型
await _ttsService.speak(text, voiceType: currentVoiceType); await _ttsService.speak(text, voiceType: currentVoiceType);

259
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<bool> _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<void> _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;
}
}
Loading…
Cancel
Save