Browse Source

before add volcalno speech

weicu
wolfplus 2 years ago
parent
commit
55c03f855e
  1. 9
      android/app/build.gradle.kts
  2. 4
      android/build.gradle.kts
  3. 3
      android/settings.gradle.kts
  4. 206
      local_plugins/volcano_speech/README.md
  5. 64
      local_plugins/volcano_speech/android/build.gradle.kts
  6. 1
      local_plugins/volcano_speech/android/settings.gradle.kts
  7. 9
      local_plugins/volcano_speech/android/src/main/AndroidManifest.xml
  8. 529
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt
  9. 445
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt
  10. 399
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt
  11. 55
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/utils/FileLogger.kt
  12. 603
      local_plugins/volcano_speech/lib/volcano_speech.dart
  13. 28
      local_plugins/volcano_speech/pubspec.yaml
  14. 2
      pubspec.yaml

9
android/app/build.gradle.kts

@ -93,14 +93,13 @@ dependencies {
// 添加本地插件模块依赖 // 添加本地插件模块依赖
implementation(project(":azure_speech")) implementation(project(":azure_speech"))
implementation(project(":open_ai_service")) implementation(project(":open_ai_service"))
implementation(project(":volcano_speech"))
// 添加OkHttp依赖 // 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3") implementation("com.squareup.okhttp3:okhttp:4.9.3")
// 添加核心库反糖化 // 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// Microsoft 语音识别SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.42.0")
// 添加androidx.media依赖 // 添加androidx.media依赖
implementation("androidx.media:media:1.6.0") implementation("androidx.media:media:1.6.0")
@ -111,11 +110,7 @@ dependencies {
// 添加 AndroidX Security 加密 SharedPreferences 依赖 // 添加 AndroidX Security 加密 SharedPreferences 依赖
implementation("androidx.security:security-crypto:1.1.0-alpha06") implementation("androidx.security:security-crypto:1.1.0-alpha06")
// // 添加火山语音合成SDK依赖
// implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8")
// 添加火山语音识别SDK依赖
// implementation("com.bytedance.speechengine:speechengine_tob:0.0.5")
} }
flutter { flutter {

4
android/build.gradle.kts

@ -3,6 +3,10 @@ allprojects {
google() google()
mavenCentral() mavenCentral()
maven { url = uri("https://storage.googleapis.com/download.flutter.io") } maven { url = uri("https://storage.googleapis.com/download.flutter.io") }
// 添加火山引擎Maven仓库
maven {
url = uri("https://artifact.bytedance.com/repository/Volcengine/")
}
} }
} }

3
android/settings.gradle.kts

@ -31,8 +31,9 @@ plugins {
include(":app") include(":app")
include(":azure_speech") include(":azure_speech")
include(":open_ai_service") include(":open_ai_service")
include(":volcano_speech")
// 设置azure_speech项目的路径 // 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android") project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
project(":volcano_speech").projectDir = file("../local_plugins/volcano_speech/android")

206
local_plugins/volcano_speech/README.md

@ -0,0 +1,206 @@
# 火山引擎语音服务插件
基于火山引擎语音SDK封装的Flutter插件,支持Android平台。
## 功能
- 语音合成(TTS)
- 支持在线/离线/混合合成模式
- 支持SSML格式文本
- 支持情感合成和情感预测
- 支持音量、语速、音高等多种参数调节
- 复刻音色支持
- 离线资源管理
- 语音识别(ASR)
- 一次性识别
- 连续识别
- 长按识别
- 热词优化
- 多语言支持
## 使用说明
### TTS 语音合成
#### 基础用法
```dart
import 'package:volcano_speech/volcano_speech.dart';
// 初始化
await VolcanoSpeech.initialize(appId: 'YOUR_APP_ID', apiKey: 'YOUR_API_KEY');
// 设置音量、语速等参数
await VolcanoSpeech.setSpeechParams(
rate: 0, // 语速: -500到500,0为正常速度
volume: 100, // 音量: 0到100,默认100
pitch: 0, // 音高: -500到500,0为正常音高
silenceDuration: 500, // 静音段时长: 毫秒
);
// 设置音色
await VolcanoSpeech.setVoice(
voiceName: 'zh_female_qingxin',
voiceType: 'qingxin'
);
// 开始合成并播放
await VolcanoSpeech.speakText(text: '这是一段测试文本');
// 暂停播放
await VolcanoSpeech.pausePlayback();
// 继续播放
await VolcanoSpeech.resumePlayback();
// 停止播放
await VolcanoSpeech.stopSpeaking();
// 销毁引擎
await VolcanoSpeech.dispose();
```
#### 进阶用法
```dart
// 设置工作模式
await VolcanoSpeech.setWorkMode(mode: TtsWorkMode.alternate); // 先在线,断网时切换离线
// 设置离线发音人
await VolcanoSpeech.setOfflineVoice(
voiceName: 'xifei',
voiceType: 'qingxin'
);
// 下载离线资源
await VolcanoSpeech.downloadOfflineResource(
voiceTypes: ['qingxin', 'zhenjiang'],
languages: ['zh-CN']
);
// 使用SSML格式文本
await VolcanoSpeech.setTextType(type: TtsTextType.ssml);
await VolcanoSpeech.speakText(
text: '<speak>这是一段<say-as interpret-as="date">2023-10-01</say-as>的语音合成</speak>'
);
// 设置情感
await VolcanoSpeech.setEmotion(emotion: 'happy');
// 启用情感预测
await VolcanoSpeech.setEnableEmotionPredict(enable: true);
// 启用服务端缓存
await VolcanoSpeech.setEnableCache(enable: true);
// 监听TTS进度事件
VolcanoSpeech.ttsProgressEvents.listen((event) {
print('播放进度: ${(event.progress * 100).toStringAsFixed(1)}%');
});
// 复刻音色支持
await VolcanoSpeech.setEnableVoiceClone(
enable: true,
backendCluster: 'your_cluster_name'
);
```
### ASR 语音识别
#### 一次性识别
```dart
import 'package:volcano_speech/volcano_speech.dart';
// 初始化
await VolcanoSpeech.initializeAsr(
appId: 'YOUR_APP_ID',
apiKey: 'YOUR_API_KEY',
supportedLanguages: ['zh-CN'],
);
// 设置识别语言
await VolcanoSpeech.setAsrLanguage(language: 'zh-CN');
// 设置热词(可选)
await VolcanoSpeech.setAsrHotWords(
hotWords: '{"hotwords":[{"word":"火山引擎","scale":2.0}]}',
);
// 开始一次性识别
try {
final result = await VolcanoSpeech.recognizeOnce();
print('识别结果: ${result['text']}, 语言: ${result['language']}');
} catch (e) {
print('识别出错: $e');
}
// 销毁引擎
await VolcanoSpeech.disposeAsr();
```
#### 连续识别
```dart
import 'package:volcano_speech/volcano_speech.dart';
// 初始化
await VolcanoSpeech.initializeAsr(
appId: 'YOUR_APP_ID',
apiKey: 'YOUR_API_KEY',
);
// 监听识别事件
VolcanoSpeech.asrEvents.listen((event) {
switch (event.type) {
case AsrEventType.sessionStarted:
print('识别会话开始');
break;
case AsrEventType.sessionStopped:
print('识别会话结束');
break;
case AsrEventType.recognizing:
print('正在识别: ${event.text}');
break;
case AsrEventType.result:
print('识别结果: ${event.text}');
break;
case AsrEventType.volumeChanged:
print('音量: ${event.volume}');
break;
case AsrEventType.error:
print('识别错误: ${event.errorMessage}');
break;
}
});
// 开始连续识别
await VolcanoSpeech.startContinuousRecognition();
// 检查是否正在识别
final isActive = await VolcanoSpeech.isContinuousRecognitionActive();
print('是否正在识别: $isActive');
// 停止连续识别
await VolcanoSpeech.stopContinuousRecognition();
```
#### 长按识别
```dart
import 'package:volcano_speech/volcano_speech.dart';
// 初始化
await VolcanoSpeech.initializeAsr(
appId: 'YOUR_APP_ID',
apiKey: 'YOUR_API_KEY',
);
// 监听识别事件
VolcanoSpeech.asrEvents.listen((event) {
// 处理事件...
});
// 用户按下按钮时开始识别
onPressed: () async {
await VolcanoSpeech.startListening();
},
// 用户释放按钮时停止识别
onReleased: () async {
await VolcanoSpeech.stopListening();
},
```

64
local_plugins/volcano_speech/android/build.gradle.kts

@ -0,0 +1,64 @@
import com.android.build.gradle.LibraryExtension
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
plugins {
id("com.android.library")
kotlin("android")
}
// 配置android扩展
configure<LibraryExtension> {
namespace = "com.yunqiinnovation.volcano_speech"
compileSdkVersion(33)
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
sourceSets {
getByName("main") {
manifest.srcFile("src/main/AndroidManifest.xml")
java.srcDirs("src/main/kotlin")
}
}
// 添加lint选项
lintOptions {
isCheckReleaseBuilds = false
}
}
// 显式设置Kotlin JVM目标版本
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
// 添加火山引擎语音合成SDK
implementation("com.bytedance.speechengine:speechengine_tob:0.0.5")
}

1
local_plugins/volcano_speech/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "volcano_speech"

9
local_plugins/volcano_speech/android/src/main/AndroidManifest.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.volcano_speech">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
</manifest>

529
local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt

@ -0,0 +1,529 @@
package com.yunqiinnovation.volcano_speech
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.bytedance.speech.speechengine.SpeechEngine
import com.bytedance.speech.speechengine.SpeechEngineDefines
import com.bytedance.speech.speechengine.SpeechEngineGenerator
import com.yunqiinnovation.volcano_speech.utils.FileLogger
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
/**
* 火山语音识别帮助类 (大模型版本)
*/
class VolcanoAsrHelper(private val context: Context) {
private val TAG = "VolcanoAsrHelper"
private val mainHandler = Handler(Looper.getMainLooper())
// 语音引擎相关
private var engine: SpeechEngine? = null
private var engineHandler: Long = -1
private var isInitialized = false
// 当前回调
private var currentAsrCallback: ASRCallback? = null
private var currentContinuousCallback: ASRContinuousCallback? = null
// 识别状态
private var isContinuousRecognitionActive = false
// 配置参数
private var language = "zh-CN"
private var enableVolume = false
private var showUtterances = false
/**
* ASR一次性识别回调
*/
interface ASRCallback {
fun onSuccess(text: String)
fun onError(error: String)
}
/**
* ASR连续识别回调
*/
interface ASRContinuousCallback {
fun onResult(text: String)
fun onRecognizing(text: String)
fun onSessionStarted()
fun onSessionStopped()
fun onVolumeChanged(volume: Int)
fun onError(error: String)
}
/**
* 初始化语音识别引擎
*/
fun initialize(appId: String, token: String, resourceId: String): Boolean {
if (isInitialized) {
FileLogger.i(TAG, "引擎已经初始化")
return true
}
try {
// 准备环境
SpeechEngineGenerator.PrepareEnvironment(context, null)
// 创建引擎
engine = SpeechEngineGenerator.getInstance()
engineHandler = engine?.createEngine() ?: -1
if (engineHandler == -1L) {
FileLogger.e(TAG, "创建引擎失败")
return false
}
// 设置上下文
engine?.setContext(context)
// 设置引擎类型为ASR
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING, SpeechEngineDefines.ASR_ENGINE)
// 设置日志级别
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, SpeechEngineDefines.LOG_LEVEL_WARN)
// 设置用户ID和设备ID (使用静态值,实际项目中应替换为真实值)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_UID_STRING, "user_id")
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING, "device_id")
// 设置鉴权信息 - 大模型版本不需要Bearer前缀
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, appId)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, token)
// 设置资源ID - 大模型必需
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING, resourceId)
// 设置协议类型为Seed - 大模型必需
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT, SpeechEngineDefines.PROTOCOL_TYPE_SEED)
// 设置网络配置
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING, "wss://openspeech.bytedance.com")
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING, "/api/v3/sauc/bigmodel")
// 设置超时时间
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_CONN_TIMEOUT_INT, 12000)
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_RECV_TIMEOUT_INT, 8000)
// 设置音频来源为录音机
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RECORDER_TYPE_STRING, SpeechEngineDefines.RECORDER_TYPE_RECORDER)
// 设置最大录音时长 (默认60秒)
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT, 60000)
// 设置回声消除 (用于ASR识别时不会收到TTS的声音)
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_RECORDER_PRESET_INT, SpeechEngineDefines.RECORDER_PRESET_VOICE_COMMUNICATION)
// 初始化引擎
val result = engine?.initEngine(engineHandler)
isInitialized = result == SpeechEngineDefines.ERR_NO_ERROR
if (isInitialized) {
FileLogger.i(TAG, "引擎初始化成功")
// 设置回调监听
engine?.setListener(object : SpeechEngine.SpeechListener {
override fun onSpeechMessage(type: Int, data: ByteArray, len: Int) {
val stdData = String(data)
handleEngineEvent(type, stdData)
}
})
return true
} else {
FileLogger.e(TAG, "引擎初始化失败: $result")
return false
}
} catch (e: Exception) {
FileLogger.e(TAG, "初始化异常: ${e.message}", e)
return false
}
}
/**
* 设置语言
*/
fun setLanguage(language: String): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
this.language = language
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_LANGUAGE_STRING, language)
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置语言失败: ${e.message}", e)
return false
}
}
/**
* 设置热词
*/
fun setHotWords(hotWordsId: String): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
if (hotWordsId.isNotEmpty()) {
// 大模型ASR需要通过请求参数设置热词
val reqParams = "{\"corpus\":{\"boosting_table_id\":\"$hotWordsId\"}}"
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_REQ_PARAMS_STRING, reqParams)
}
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置热词失败: ${e.message}", e)
return false
}
}
/**
* 设置是否返回音量
*/
fun setEnableVolume(enable: Boolean): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
this.enableVolume = enable
engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ENABLE_GET_VOLUME_BOOL, enable)
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置音量返回失败: ${e.message}", e)
return false
}
}
/**
* 设置是否显示语音停顿、分句、分词信息
*/
fun setShowUtterances(show: Boolean): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
this.showUtterances = show
engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_SHOW_UTTER_BOOL, show)
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置语音信息显示失败: ${e.message}", e)
return false
}
}
/**
* 设置VAD切句参数
*/
fun setVadParams(forceToSpeechTime: Int, endWindowSize: Int): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
val reqParams = "{\"force_to_speech_time\":$forceToSpeechTime, \"end_window_size\":$endWindowSize}"
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_REQ_PARAMS_STRING, reqParams)
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置VAD参数失败: ${e.message}", e)
return false
}
}
/**
* 设置纠错词表
*/
fun setCorrectWords(correctWordsJson: String): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
val reqParams = "{\"context\": \"{\\\"correct_words\\\": $correctWordsJson}\"}"
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_REQ_PARAMS_STRING, reqParams)
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置纠错词表失败: ${e.message}", e)
return false
}
}
/**
* 一次性识别
*/
fun recognizeOnce(callback: ASRCallback): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
if (isContinuousRecognitionActive) {
FileLogger.e(TAG, "当前正在连续识别中")
return false
}
try {
this.currentAsrCallback = callback
// 停止当前引擎
engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "")
// 启动引擎开始识别
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "启动识别失败: $ret")
return false
}
return true
} catch (e: Exception) {
FileLogger.e(TAG, "识别异常: ${e.message}", e)
return false
}
}
/**
* 停止一次性识别
*/
fun stopRecognize(): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
// 告知引擎音频输入完成
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_FINISH_TALKING, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "停止识别失败: $ret")
return false
}
return true
} catch (e: Exception) {
FileLogger.e(TAG, "停止识别异常: ${e.message}", e)
return false
}
}
/**
* 开始连续识别
*/
fun startContinuousRecognition(callback: ASRContinuousCallback): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
if (isContinuousRecognitionActive) {
FileLogger.e(TAG, "当前已经在连续识别中")
return false
}
try {
this.currentContinuousCallback = callback
// 停止当前引擎
engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "")
// 启动引擎开始识别
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "启动连续识别失败: $ret")
return false
}
isContinuousRecognitionActive = true
return true
} catch (e: Exception) {
FileLogger.e(TAG, "连续识别异常: ${e.message}", e)
return false
}
}
/**
* 停止连续识别
*/
fun stopContinuousRecognition(): Boolean {
if (!isInitialized || !isContinuousRecognitionActive) {
FileLogger.e(TAG, "引擎未初始化或未在连续识别中")
return false
}
try {
// 停止引擎
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "停止连续识别失败: $ret")
return false
}
isContinuousRecognitionActive = false
return true
} catch (e: Exception) {
FileLogger.e(TAG, "停止连续识别异常: ${e.message}", e)
return false
}
}
/**
* 判断是否在连续识别中
*/
fun isContinuousRecognitionActive(): Boolean {
return isContinuousRecognitionActive
}
/**
* 释放资源
*/
fun release() {
if (!isInitialized) {
return
}
try {
// 停止引擎
if (isContinuousRecognitionActive) {
stopContinuousRecognition()
} else {
engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
}
// 销毁引擎
engine?.destroyEngine(engineHandler)
engineHandler = -1
engine = null
isInitialized = false
FileLogger.i(TAG, "引擎已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放引擎异常: ${e.message}", e)
}
}
/**
* 处理引擎事件
*/
private fun handleEngineEvent(type: Int, data: String) {
when (type) {
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> {
// 引擎启动成功
mainHandler.post {
currentContinuousCallback?.onSessionStarted()
}
}
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> {
// 引擎停止
isContinuousRecognitionActive = false
mainHandler.post {
currentContinuousCallback?.onSessionStopped()
}
}
SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT -> {
// 中间识别结果
try {
val json = JSONObject(data)
if (!json.has("result")) {
return
}
val resultArray = json.getJSONArray("result")
if (resultArray.length() > 0) {
val result = resultArray.getJSONObject(0)
val text = result.optString("text", "")
if (text.isNotEmpty()) {
mainHandler.post {
currentContinuousCallback?.onRecognizing(text)
}
}
}
} catch (e: JSONException) {
FileLogger.e(TAG, "解析中间识别结果异常: ${e.message}", e)
}
}
SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT -> {
// 最终识别结果
try {
val json = JSONObject(data)
if (!json.has("result")) {
return
}
val resultArray = json.getJSONArray("result")
if (resultArray.length() > 0) {
val result = resultArray.getJSONObject(0)
val text = result.optString("text", "")
if (text.isNotEmpty()) {
mainHandler.post {
if (isContinuousRecognitionActive) {
currentContinuousCallback?.onResult(text)
} else {
currentAsrCallback?.onSuccess(text)
}
}
}
}
} catch (e: JSONException) {
FileLogger.e(TAG, "解析最终识别结果异常: ${e.message}", e)
}
}
SpeechEngineDefines.MESSAGE_TYPE_VOLUME_LEVEL -> {
// 音量回调
if (enableVolume && isContinuousRecognitionActive) {
try {
val volume = (data.toFloat() * 100).toInt()
mainHandler.post {
currentContinuousCallback?.onVolumeChanged(volume)
}
} catch (e: Exception) {
FileLogger.e(TAG, "解析音量数据异常: ${e.message}", e)
}
}
}
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR -> {
// 错误信息
try {
val json = JSONObject(data)
val errCode = json.optInt("err_code", -1)
val errMsg = json.optString("err_msg", "未知错误")
FileLogger.e(TAG, "引擎错误: $errCode, $errMsg")
mainHandler.post {
if (isContinuousRecognitionActive) {
currentContinuousCallback?.onError(errMsg)
isContinuousRecognitionActive = false
} else {
currentAsrCallback?.onError(errMsg)
}
}
} catch (e: JSONException) {
FileLogger.e(TAG, "解析错误信息异常: ${e.message}", e)
}
}
}
}
}

445
local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt

@ -0,0 +1,445 @@
package com.yunqiinnovation.volcano_speech
import android.content.Context
import android.os.Handler
import android.os.Looper
import androidx.annotation.NonNull
import com.yunqiinnovation.volcano_speech.utils.FileLogger
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel
/** VolcanoSpeechPlugin */
class VolcanoSpeechPlugin: FlutterPlugin {
private val TAG = "VolcanoSpeechPlugin"
private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper())
// ASR相关
private lateinit var asrChannel: MethodChannel
private lateinit var asrEventChannel: EventChannel
private var asrEventSink: EventChannel.EventSink? = null
// TTS相关
private lateinit var ttsChannel: MethodChannel
private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null
// TTS帮助类
private lateinit var volcanoTtsHelper: VolcanoTtsHelper
// ASR帮助类
private lateinit var volcanoAsrHelper: VolcanoAsrHelper
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
// 初始化ASR通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "volcano_speech/asr")
asrChannel.setMethodCallHandler(AsrMethodHandler())
// 初始化TTS通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "volcano_speech/tts")
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR事件通道
asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "volcano_speech/asr_events")
asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
asrEventSink = events
}
override fun onCancel(arguments: Any?) {
asrEventSink = null
}
})
// 初始化TTS事件通道
ttsEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "volcano_speech/tts_events")
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events
}
override fun onCancel(arguments: Any?) {
ttsEventSink = null
}
})
// 初始化TTS帮助类
volcanoTtsHelper = VolcanoTtsHelper(context)
// 初始化ASR帮助类
volcanoAsrHelper = VolcanoAsrHelper(context)
}
// 发送ASR事件
private fun sendAsrEvent(event: Map<String, Any>) {
FileLogger.d(TAG, "发送ASR事件: $event")
if (asrEventSink == null) {
FileLogger.w(TAG, "无法发送ASR事件:事件通道未准备好")
return
}
mainHandler.post {
try {
asrEventSink?.success(event)
FileLogger.d(TAG, "ASR事件发送成功")
} catch (e: Exception) {
FileLogger.e(TAG, "发送ASR事件失败: ${e.message}")
}
}
}
// 发送TTS事件
private fun sendTtsEvent(event: Map<String, Any>) {
FileLogger.d(TAG, "发送TTS事件: $event")
if (ttsEventSink == null) {
FileLogger.w(TAG, "无法发送TTS事件:事件通道未准备好")
return
}
mainHandler.post {
try {
ttsEventSink?.success(event)
FileLogger.d(TAG, "TTS事件发送成功")
} catch (e: Exception) {
FileLogger.e(TAG, "发送TTS事件失败: ${e.message}")
}
}
}
// ASR方法处理器
inner class AsrMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"getPlatformVersion" -> {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
}
"initialize" -> {
val appId = call.argument<String>("appId") ?: ""
val apiKey = call.argument<String>("apiKey") ?: ""
val resourceId = call.argument<String>("resourceId") ?: ""
if (appId.isEmpty() || apiKey.isEmpty() || resourceId.isEmpty()) {
result.error("INVALID_ARGUMENTS", "appId、apiKey和resourceId不能为空", null)
return
}
val success = volcanoAsrHelper.initialize(appId, apiKey, resourceId)
result.success(success)
}
"setLanguage" -> {
val language = call.argument<String>("language") ?: "zh-CN"
result.success(volcanoAsrHelper.setLanguage(language))
}
"setHotWords" -> {
val hotWordsId = call.argument<String>("hotWordsId") ?: ""
result.success(volcanoAsrHelper.setHotWords(hotWordsId))
}
"setVadParams" -> {
val forceToSpeechTime = call.argument<Int>("forceToSpeechTime") ?: 0
val endWindowSize = call.argument<Int>("endWindowSize") ?: 800
result.success(volcanoAsrHelper.setVadParams(forceToSpeechTime, endWindowSize))
}
"setCorrectWords" -> {
val correctWordsJson = call.argument<String>("correctWordsJson") ?: "{}"
result.success(volcanoAsrHelper.setCorrectWords(correctWordsJson))
}
"setEnableVolume" -> {
val enable = call.argument<Boolean>("enable") ?: false
result.success(volcanoAsrHelper.setEnableVolume(enable))
}
"setShowUtterances" -> {
val enable = call.argument<Boolean>("enable") ?: false
result.success(volcanoAsrHelper.setShowUtterances(enable))
}
"recognizeOnce" -> {
// 确保当前不在连续识别中
if (volcanoAsrHelper.isContinuousRecognitionActive()) {
result.error("ASR_BUSY", "当前正在连续识别中", null)
return
}
volcanoAsrHelper.recognizeOnce(object : VolcanoAsrHelper.ASRCallback {
override fun onSuccess(text: String) {
mainHandler.post {
result.success(mapOf(
"text" to text
))
}
}
override fun onError(error: String) {
mainHandler.post {
result.error("ASR_ERROR", error, null)
}
}
})
}
"startContinuousRecognition" -> {
// 确保事件通道已准备好
if (asrEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
return
}
val success = volcanoAsrHelper.startContinuousRecognition(object : VolcanoAsrHelper.ASRContinuousCallback {
override fun onResult(text: String) {
sendAsrEvent(mapOf(
"type" to "result",
"text" to text
))
}
override fun onRecognizing(text: String) {
sendAsrEvent(mapOf(
"type" to "recognizing",
"text" to text
))
}
override fun onSessionStarted() {
sendAsrEvent(mapOf("type" to "sessionStarted"))
}
override fun onSessionStopped() {
sendAsrEvent(mapOf("type" to "sessionStopped"))
}
override fun onVolumeChanged(volume: Int) {
sendAsrEvent(mapOf(
"type" to "volumeChanged",
"volume" to volume
))
}
override fun onError(error: String) {
sendAsrEvent(mapOf(
"type" to "error",
"message" to error
))
}
})
result.success(success)
}
"stopContinuousRecognition" -> {
val success = volcanoAsrHelper.stopContinuousRecognition()
result.success(success)
}
"stopRecognize" -> {
val success = volcanoAsrHelper.stopRecognize()
result.success(success)
}
"isContinuousRecognitionActive" -> {
result.success(volcanoAsrHelper.isContinuousRecognitionActive())
}
"release" -> {
volcanoAsrHelper.release()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
}
// TTS方法处理器
inner class TtsMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"getPlatformVersion" -> {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
}
"initialize" -> {
val appId = call.argument<String>("appId") ?: ""
val token = call.argument<String>("token") ?: ""
val resourceId = call.argument<String>("resourceId") ?: ""
if (appId.isEmpty() || token.isEmpty() || resourceId.isEmpty()) {
result.error("INVALID_ARGUMENTS", "appId、token和resourceId不能为空", null)
return
}
val success = volcanoTtsHelper.initialize(appId, token, resourceId)
result.success(success)
}
"setVoice" -> {
val voice = call.argument<String>("voice") ?: ""
if (voice.isEmpty()) {
result.error("INVALID_ARGUMENTS", "voice不能为空", null)
return
}
result.success(volcanoTtsHelper.setVoice(voice))
}
"setContinuousMode" -> {
val isContinuous = call.argument<Boolean>("isContinuous") ?: false
result.success(volcanoTtsHelper.setContinuousMode(isContinuous))
}
"speak" -> {
val text = call.argument<String>("text") ?: ""
if (text.isEmpty()) {
result.error("INVALID_ARGUMENTS", "合成文本不能为空", null)
return
}
volcanoTtsHelper.speak(text, object : VolcanoTtsHelper.TTSCallback {
override fun onStart(reqId: String) {
sendTtsEvent(mapOf(
"type" to "start",
"reqId" to reqId
))
}
override fun onProgress(reqId: String, progress: Double) {
sendTtsEvent(mapOf(
"type" to "progress",
"reqId" to reqId,
"progress" to progress
))
}
override fun onComplete(reqId: String) {
sendTtsEvent(mapOf(
"type" to "complete",
"reqId" to reqId
))
mainHandler.post {
result.success(true)
}
}
override fun onError(reqId: String, errorCode: Int, errorMsg: String) {
sendTtsEvent(mapOf(
"type" to "error",
"reqId" to reqId,
"errorCode" to errorCode,
"errorMsg" to errorMsg
))
mainHandler.post {
result.error("TTS_ERROR", errorMsg, null)
}
}
})
}
"synthesisNext" -> {
val text = call.argument<String>("text") ?: ""
if (text.isEmpty()) {
result.error("INVALID_ARGUMENTS", "合成文本不能为空", null)
return
}
val success = volcanoTtsHelper.synthesisNext(text)
result.success(success)
}
"pause" -> {
result.success(volcanoTtsHelper.pause())
}
"resume" -> {
result.success(volcanoTtsHelper.resume())
}
"stop" -> {
result.success(volcanoTtsHelper.stop())
}
"release" -> {
volcanoTtsHelper.release()
result.success(true)
}
// 以下方法用于与Azure Speech版本兼容
"setSpeechSynthesisVoice" -> {
val voiceName = call.argument<String>("voiceName") ?: ""
if (voiceName.isEmpty()) {
result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
return
}
result.success(volcanoTtsHelper.setVoice(voiceName))
}
"speakText" -> {
val text = call.argument<String>("text") ?: ""
if (text.isEmpty()) {
result.error("INVALID_ARGUMENTS", "合成文本不能为空", null)
return
}
volcanoTtsHelper.speak(text, object : VolcanoTtsHelper.TTSCallback {
override fun onStart(reqId: String) {
sendTtsEvent(mapOf(
"type" to "start",
"reqId" to reqId
))
}
override fun onProgress(reqId: String, progress: Double) {
sendTtsEvent(mapOf(
"type" to "progress",
"reqId" to reqId,
"progress" to progress
))
}
override fun onComplete(reqId: String) {
sendTtsEvent(mapOf(
"type" to "complete",
"reqId" to reqId
))
mainHandler.post {
result.success(true)
}
}
override fun onError(reqId: String, errorCode: Int, errorMsg: String) {
sendTtsEvent(mapOf(
"type" to "error",
"reqId" to reqId,
"errorCode" to errorCode,
"errorMsg" to errorMsg
))
mainHandler.post {
result.error("TTS_ERROR", errorMsg, null)
}
}
})
}
"stopSpeaking" -> {
result.success(volcanoTtsHelper.stop())
}
"pauseSpeaking" -> {
result.success(volcanoTtsHelper.pause())
}
"resumeSpeaking" -> {
result.success(volcanoTtsHelper.resume())
}
else -> {
result.notImplemented()
}
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
asrChannel.setMethodCallHandler(null)
ttsChannel.setMethodCallHandler(null)
asrEventChannel.setStreamHandler(null)
ttsEventChannel.setStreamHandler(null)
volcanoTtsHelper.release()
volcanoAsrHelper.release()
}
}

399
local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt

@ -0,0 +1,399 @@
package com.yunqiinnovation.volcano_speech
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.bytedance.speech.speechengine.SpeechEngine
import com.bytedance.speech.speechengine.SpeechEngineDefines
import com.bytedance.speech.speechengine.SpeechEngineGenerator
import com.yunqiinnovation.volcano_speech.utils.FileLogger
import org.json.JSONObject
/**
* 火山语音合成帮助类 (大模型版本)
*/
class VolcanoTtsHelper(private val context: Context) {
private val TAG = "VolcanoTtsHelper"
private val mainHandler = Handler(Looper.getMainLooper())
// 语音引擎相关
private var engine: SpeechEngine? = null
private var engineHandler: Long = -1
private var isInitialized = false
// 当前回调
private var currentTtsCallback: TTSCallback? = null
// 合成状态
private var isPlaying = false
private var currentReqId = ""
// 配置参数
private var voice = "zh_female_yuxi"
private var ttsText = ""
private var isContinuous = false
/**
* TTS合成回调接口
*/
interface TTSCallback {
fun onStart(reqId: String)
fun onProgress(reqId: String, progress: Double)
fun onComplete(reqId: String)
fun onError(reqId: String, errorCode: Int, errorMsg: String)
}
/**
* 初始化语音合成引擎
*/
fun initialize(appId: String, token: String, resourceId: String): Boolean {
if (isInitialized) {
FileLogger.i(TAG, "引擎已经初始化")
return true
}
try {
// 准备环境
SpeechEngineGenerator.PrepareEnvironment(context, null)
// 创建引擎
engine = SpeechEngineGenerator.getInstance()
engineHandler = engine?.createEngine() ?: -1
if (engineHandler == -1L) {
FileLogger.e(TAG, "创建引擎失败")
return false
}
// 设置上下文
engine?.setContext(context)
// 设置引擎类型为TTS
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING, SpeechEngineDefines.TTS_ENGINE)
// 设置日志级别
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, SpeechEngineDefines.LOG_LEVEL_WARN)
// 设置用户ID和设备ID (使用静态值,实际项目中应替换为真实值)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_UID_STRING, "user_id")
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING, "device_id")
// 设置授权信息 - 大模型版本不需要Bearer前缀
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, appId)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, token)
// 设置资源ID - 大模型必需
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING, resourceId)
// 设置协议类型为Seed - 大模型必需
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT, SpeechEngineDefines.PROTOCOL_TYPE_SEED)
// 设置合成策略为在线合成
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_WORK_MODE_INT, SpeechEngineDefines.TTS_WORK_MODE_ONLINE)
// 设置在线请求资源配置
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_ADDRESS_STRING, "wss://openspeech.bytedance.com")
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_URI_STRING, "/api/v3/sauc/bigmodel")
// 设置发音人
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_ONLINE_STRING, voice)
// 设置播放进度回调
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_WITH_FRONTEND_INT, 1)
// 初始化引擎
val result = engine?.initEngine(engineHandler)
isInitialized = result == SpeechEngineDefines.ERR_NO_ERROR
if (isInitialized) {
FileLogger.i(TAG, "引擎初始化成功")
// 设置回调监听
engine?.setListener(object : SpeechEngine.SpeechListener {
override fun onSpeechMessage(type: Int, data: ByteArray, len: Int) {
val stdData = String(data)
handleEngineEvent(type, stdData)
}
})
return true
} else {
FileLogger.e(TAG, "引擎初始化失败: $result")
return false
}
} catch (e: Exception) {
FileLogger.e(TAG, "初始化异常: ${e.message}", e)
return false
}
}
/**
* 设置发音人
*/
fun setVoice(voice: String): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
this.voice = voice
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_ONLINE_STRING, voice)
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置发音人失败: ${e.message}", e)
return false
}
}
/**
* 设置合成场景(单次或连续)
*/
fun setContinuousMode(isContinuous: Boolean): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
this.isContinuous = isContinuous
val scenarioType = if (isContinuous) {
SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL
} else {
SpeechEngineDefines.TTS_SCENARIO_TYPE_NORMAL
}
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, scenarioType)
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置合成场景失败: ${e.message}", e)
return false
}
}
/**
* 开始合成并播放
*/
fun speak(text: String, callback: TTSCallback? = null): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
if (text.isEmpty()) {
FileLogger.e(TAG, "合成文本为空")
return false
}
try {
this.ttsText = text
this.currentTtsCallback = callback
// 设置要合成的文本
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, text)
// 停止当前引擎
engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "")
// 启动引擎开始合成
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "启动合成失败: $ret")
return false
}
return true
} catch (e: Exception) {
FileLogger.e(TAG, "合成异常: ${e.message}", e)
return false
}
}
/**
* 仅用于连续合成场景:在引擎启动后添加新的文本进行合成
*/
fun synthesisNext(text: String): Boolean {
if (!isInitialized || !isContinuous) {
FileLogger.e(TAG, "引擎未初始化或非连续合成模式")
return false
}
if (text.isEmpty()) {
FileLogger.e(TAG, "合成文本为空")
return false
}
try {
// 设置要合成的文本
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, text)
// 发送合成指令
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNTHESIS, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "添加合成文本失败: $ret")
return false
}
return true
} catch (e: Exception) {
FileLogger.e(TAG, "添加合成文本异常: ${e.message}", e)
return false
}
}
/**
* 暂停播放
*/
fun pause(): Boolean {
if (!isInitialized || !isPlaying) {
FileLogger.e(TAG, "引擎未初始化或未在播放")
return false
}
try {
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_PAUSE_PLAYER, "")
return ret == SpeechEngineDefines.ERR_NO_ERROR
} catch (e: Exception) {
FileLogger.e(TAG, "暂停播放异常: ${e.message}", e)
return false
}
}
/**
* 恢复播放
*/
fun resume(): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_RESUME_PLAYER, "")
return ret == SpeechEngineDefines.ERR_NO_ERROR
} catch (e: Exception) {
FileLogger.e(TAG, "恢复播放异常: ${e.message}", e)
return false
}
}
/**
* 停止播放
*/
fun stop(): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
return false
}
try {
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
isPlaying = false
return ret == SpeechEngineDefines.ERR_NO_ERROR
} catch (e: Exception) {
FileLogger.e(TAG, "停止播放异常: ${e.message}", e)
return false
}
}
/**
* 释放资源
*/
fun release() {
if (!isInitialized) {
return
}
try {
// 停止引擎
stop()
// 销毁引擎
engine?.destroyEngine(engineHandler)
engineHandler = -1
engine = null
isInitialized = false
FileLogger.i(TAG, "引擎已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放引擎异常: ${e.message}", e)
}
}
/**
* 处理引擎事件
*/
private fun handleEngineEvent(type: Int, data: String) {
when (type) {
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> {
// 引擎启动成功,获取请求ID
currentReqId = data
isPlaying = true
mainHandler.post {
currentTtsCallback?.onStart(currentReqId)
}
if (isContinuous) {
// 在连续合成模式下,需要单独发送合成指令
synthesisNext(ttsText)
}
}
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> {
// 引擎停止
isPlaying = false
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_START_PLAYING -> {
// 开始播放
FileLogger.d(TAG, "开始播放: $data")
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_FINISH_PLAYING -> {
// 播放结束
isPlaying = false
mainHandler.post {
currentTtsCallback?.onComplete(currentReqId)
}
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_PLAYBACK_PROGRESS -> {
// 播放进度
try {
val json = JSONObject(data)
val progress = json.optDouble("progress", 0.0)
val reqId = json.optString("reqid", "")
mainHandler.post {
currentTtsCallback?.onProgress(reqId, progress)
}
} catch (e: Exception) {
FileLogger.e(TAG, "解析进度信息异常: ${e.message}", e)
}
}
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR -> {
// 错误信息
try {
val json = JSONObject(data)
val reqId = json.optString("reqid", "")
val errCode = json.optInt("err_code", -1)
val errMsg = json.optString("err_msg", "未知错误")
FileLogger.e(TAG, "引擎错误: $errCode, $errMsg")
isPlaying = false
mainHandler.post {
currentTtsCallback?.onError(reqId, errCode, errMsg)
}
} catch (e: Exception) {
FileLogger.e(TAG, "解析错误信息异常: ${e.message}", e)
}
}
}
}
}

55
local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/utils/FileLogger.kt

@ -0,0 +1,55 @@
package com.yunqiinnovation.volcano_speech.utils
import android.util.Log
/**
* 文件日志记录工具
*/
object FileLogger {
private const val TAG = "VolcanoSpeech"
private var isDebugEnabled = true
/**
* 设置是否启用调试日志
*/
fun setDebugEnabled(enabled: Boolean) {
isDebugEnabled = enabled
}
/**
* 记录调试日志
*/
fun d(tag: String, message: String) {
if (isDebugEnabled) {
Log.d("$TAG-$tag", message)
}
}
/**
* 记录信息日志
*/
fun i(tag: String, message: String) {
Log.i("$TAG-$tag", message)
}
/**
* 记录警告日志
*/
fun w(tag: String, message: String) {
Log.w("$TAG-$tag", message)
}
/**
* 记录错误日志
*/
fun e(tag: String, message: String) {
Log.e("$TAG-$tag", message)
}
/**
* 记录错误日志,带异常
*/
fun e(tag: String, message: String, throwable: Throwable) {
Log.e("$TAG-$tag", message, throwable)
}
}

603
local_plugins/volcano_speech/lib/volcano_speech.dart

@ -0,0 +1,603 @@
import 'dart:async';
import 'package:flutter/services.dart';
/// ASR 事件类型
enum AsrEventType {
/// 会话开始
sessionStarted,
/// 会话结束
sessionStopped,
/// 正在识别(中间结果)
recognizing,
/// 识别结果(最终结果)
result,
/// 音量变化
volumeChanged,
/// 错误
error
}
/// TTS 工作模式
enum TtsWorkMode {
/// 在线合成
online,
/// 离线合成
offline,
/// 同时在线离线
both,
/// 先在线再离线(网络不好时自动切换)
alternate,
/// 文件模式
file
}
/// TTS 文本类型
enum TtsTextType {
/// 纯文本
plain,
/// SSML格式
ssml
}
/// 协议类型
enum ProtocolType {
/// 默认协议
defaultProtocol,
/// Seed协议(用于大模型)
seed
}
/// TTS 播放进度事件
class TtsProgressEvent {
/// 播放进度 0.0-1.0
final double progress;
/// 请求ID
final String reqId;
const TtsProgressEvent({
required this.progress,
required this.reqId,
});
@override
String toString() {
return 'TtsProgressEvent{progress: $progress, reqId: $reqId}';
}
}
/// ASR 事件
class AsrEvent {
/// 事件类型
final AsrEventType type;
/// 识别文本(仅在recognizing和result类型时有效)
final String? text;
/// 识别语言(仅在recognizing和result类型时有效)
final String? language;
/// 音量值(仅在volumeChanged类型时有效)
final int? volume;
/// 错误信息(仅在error类型时有效)
final String? errorMessage;
const AsrEvent({
required this.type,
this.text,
this.language,
this.volume,
this.errorMessage,
});
factory AsrEvent.fromMap(Map<dynamic, dynamic> map) {
final typeStr = map['type'] as String;
AsrEventType type;
switch (typeStr) {
case 'sessionStarted':
type = AsrEventType.sessionStarted;
break;
case 'sessionStopped':
type = AsrEventType.sessionStopped;
break;
case 'recognizing':
type = AsrEventType.recognizing;
break;
case 'result':
type = AsrEventType.result;
break;
case 'volumeChanged':
type = AsrEventType.volumeChanged;
break;
case 'error':
type = AsrEventType.error;
break;
default:
throw ArgumentError('未知的事件类型: $typeStr');
}
return AsrEvent(
type: type,
text: map['text'] as String?,
language: map['language'] as String?,
volume: map['volume'] as int?,
errorMessage: map['message'] as String?,
);
}
@override
String toString() {
return 'AsrEvent{type: $type, text: $text, language: $language, volume: $volume, errorMessage: $errorMessage}';
}
}
/// 火山引擎语音服务插件
class VolcanoSpeech {
static final VolcanoSpeechAsr asr = VolcanoSpeechAsr._();
static final VolcanoSpeechTts tts = VolcanoSpeechTts._();
/// 释放资源
static Future<void> dispose() async {
await asr.dispose();
await tts.dispose();
}
}
/// 火山引擎语音识别服务
class VolcanoSpeechAsr {
static const MethodChannel _channel = MethodChannel('volcano_speech/asr');
static const EventChannel _eventChannel = EventChannel('volcano_speech/asr_events');
/// ASR事件流控制器
static final StreamController<AsrEvent> _eventStreamController = StreamController<AsrEvent>.broadcast();
/// ASR事件流
Stream<AsrEvent> get events => _eventStreamController.stream;
/// 是否已初始化事件监听
bool _eventListenerInitialized = false;
VolcanoSpeechAsr._() {
_initEventListener();
}
/// 初始化ASR事件监听
void _initEventListener() {
if (_eventListenerInitialized) return;
_eventChannel.receiveBroadcastStream().listen((dynamic event) {
if (event is Map) {
_eventStreamController.add(AsrEvent.fromMap(event));
}
});
_eventListenerInitialized = true;
}
/// 获取平台版本信息
Future<String?> getPlatformVersion() async {
return await _channel.invokeMethod('getPlatformVersion');
}
/// 初始化语音识别引擎
///
/// [appId] 火山引擎AppID
/// [apiKey] 火山引擎ApiKey
/// [supportedLanguages] 支持的语言列表
/// [useBigModel] 是否使用大模型识别
/// [resourceId] 大模型资源ID(仅当useBigModel为true时有效)
Future<bool> initialize({
required String appId,
required String apiKey,
List<String> supportedLanguages = const ['zh-CN'],
bool useBigModel = false,
String resourceId = '',
}) async {
return await _channel.invokeMethod('initialize', {
'appId': appId,
'apiKey': apiKey,
'supportedLanguages': supportedLanguages,
'useBigModel': useBigModel,
'resourceId': resourceId,
}) ?? false;
}
/// 设置识别语言
///
/// [language] 语言代码,例如 zh-CN、en-US
Future<bool> setLanguage(String language) async {
return await _channel.invokeMethod('setLanguage', {
'language': language,
}) ?? false;
}
/// 设置热词
///
/// [hotWords] 热词JSON字符串,例如 {"hotwords":[{"word":"快速入门","scale":2.0}]}
Future<bool> setHotWords(String hotWords) async {
return await _channel.invokeMethod('setHotWords', {
'hotWords': hotWords,
}) ?? false;
}
/// 设置ASR请求参数
///
/// [params] 请求参数JSON字符串
Future<bool> setRequestParams(String params) async {
return await _channel.invokeMethod('setRequestParams', {
'params': params,
}) ?? false;
}
/// 启用语音停顿、分句、分词信息输出
///
/// [enable] 是否启用
Future<bool> setShowUtterances(bool enable) async {
return await _channel.invokeMethod('setShowUtterances', {
'enable': enable,
}) ?? false;
}
/// 一次性识别(直到说话结束)
///
/// 返回识别结果文本和语言
Future<Map<String, String>> recognizeOnce() async {
final result = await _channel.invokeMethod('recognizeOnce');
return {
'text': result['text'] ?? '',
'language': result['language'] ?? 'zh-CN',
};
}
/// 开始连续识别
///
/// 通过[events]流监听识别结果
Future<bool> startContinuousRecognition() async {
return await _channel.invokeMethod('startContinuousRecognition') ?? false;
}
/// 停止连续识别
Future<bool> stopContinuousRecognition() async {
return await _channel.invokeMethod('stopContinuousRecognition') ?? false;
}
/// 检查是否正在连续识别
Future<bool> isContinuousRecognitionActive() async {
return await _channel.invokeMethod('isContinuousRecognitionActive') ?? false;
}
/// 开始长按识别(按下开始,抬起结束)
///
/// 通过[events]流监听识别结果
Future<bool> startListening() async {
return await _channel.invokeMethod('startListening') ?? false;
}
/// 停止长按识别
Future<bool> stopListening() async {
return await _channel.invokeMethod('stopListening') ?? false;
}
/// 释放ASR资源
Future<bool> dispose() async {
return await _channel.invokeMethod('dispose') ?? false;
}
}
/// 火山引擎语音合成服务
class VolcanoSpeechTts {
static const MethodChannel _channel = MethodChannel('volcano_speech/tts');
static const EventChannel _eventChannel = EventChannel('volcano_speech/tts_events');
/// TTS进度事件流控制器
static final StreamController<TtsProgressEvent> _progressStreamController = StreamController<TtsProgressEvent>.broadcast();
/// TTS进度事件流
Stream<TtsProgressEvent> get progressEvents => _progressStreamController.stream;
/// 是否已初始化事件监听
bool _eventListenerInitialized = false;
VolcanoSpeechTts._() {
_initEventListener();
}
/// 初始化TTS事件监听
void _initEventListener() {
if (_eventListenerInitialized) return;
_eventChannel.receiveBroadcastStream().listen((dynamic event) {
if (event is Map) {
final progress = event['progress'] as double?;
final reqId = event['reqId'] as String?;
if (progress != null && reqId != null) {
_progressStreamController.add(TtsProgressEvent(
progress: progress,
reqId: reqId,
));
}
}
});
_eventListenerInitialized = true;
}
/// 获取平台版本信息
Future<String?> getPlatformVersion() async {
return await _channel.invokeMethod('getPlatformVersion');
}
/// 启用大模型TTS
///
/// [enable] 是否启用大模型TTS
/// [resourceId] 资源ID
Future<bool> enableBigModelTts({
required bool enable,
String resourceId = '',
}) async {
return await _channel.invokeMethod('enableBigModelTts', {
'enable': enable,
'resourceId': resourceId,
}) ?? false;
}
/// 初始化语音合成引擎
///
/// [appId] 火山引擎AppID
/// [apiKey] 火山引擎ApiKey
Future<bool> initialize({
required String appId,
required String apiKey,
}) async {
return await _channel.invokeMethod('initialize', {
'appId': appId,
'apiKey': apiKey,
}) ?? false;
}
/// 设置音色
///
/// [voiceName] 音色名称
/// [voiceType] 音色类型,默认 qingxin
Future<bool> setVoice({
required String voiceName,
String voiceType = 'qingxin',
}) async {
return await _channel.invokeMethod('setVoice', {
'voiceName': voiceName,
'voiceType': voiceType,
}) ?? false;
}
/// 设置离线音色
///
/// [voiceName] 音色名称
/// [voiceType] 音色类型,默认 qingxin
Future<bool> setOfflineVoice({
required String voiceName,
String voiceType = 'qingxin',
}) async {
return await _channel.invokeMethod('setOfflineVoice', {
'voiceName': voiceName,
'voiceType': voiceType,
}) ?? false;
}
/// 设置大模型声音ID
///
/// [voiceId] 声音ID
Future<bool> setBigModelVoiceId(String voiceId) async {
return await _channel.invokeMethod('setBigModelVoiceId', {
'voiceId': voiceId,
}) ?? false;
}
/// 设置大模型请求参数
///
/// [params] 请求参数,JSON字符串
Future<bool> setBigModelRequestParams(String params) async {
return await _channel.invokeMethod('setBigModelRequestParams', {
'params': params,
}) ?? false;
}
/// 设置工作模式
///
/// [mode] 工作模式
Future<bool> setWorkMode(TtsWorkMode mode) async {
String modeStr;
switch (mode) {
case TtsWorkMode.online:
modeStr = 'online';
break;
case TtsWorkMode.offline:
modeStr = 'offline';
break;
case TtsWorkMode.both:
modeStr = 'both';
break;
case TtsWorkMode.alternate:
modeStr = 'alternate';
break;
case TtsWorkMode.file:
modeStr = 'file';
break;
}
return await _channel.invokeMethod('setWorkMode', {
'mode': modeStr,
}) ?? false;
}
/// 设置文本类型
///
/// [type] 文本类型,plain或ssml
Future<bool> setTextType(TtsTextType type) async {
String typeStr;
switch (type) {
case TtsTextType.plain:
typeStr = 'plain';
break;
case TtsTextType.ssml:
typeStr = 'ssml';
break;
}
return await _channel.invokeMethod('setTextType', {
'type': typeStr,
}) ?? false;
}
/// 设置是否启用缓存
///
/// [enable] 是否启用
Future<bool> setEnableCache(bool enable) async {
return await _channel.invokeMethod('setEnableCache', {
'enable': enable,
}) ?? false;
}
/// 设置情感
///
/// [emotion] 情感,例如 neutral、happy、angry、sad等
Future<bool> setEmotion(String emotion) async {
return await _channel.invokeMethod('setEmotion', {
'emotion': emotion,
}) ?? false;
}
/// 设置是否启用情感预测
///
/// [enable] 是否启用
Future<bool> setEnableEmotionPredict(bool enable) async {
return await _channel.invokeMethod('setEnableEmotionPredict', {
'enable': enable,
}) ?? false;
}
/// 设置是否启用声音克隆
///
/// [enable] 是否启用
/// [backendCluster] 后端集群
Future<bool> setEnableVoiceClone({
required bool enable,
String backendCluster = '',
}) async {
return await _channel.invokeMethod('setEnableVoiceClone', {
'enable': enable,
'backendCluster': backendCluster,
}) ?? false;
}
/// 设置是否启用回声消除
///
/// [enable] 是否启用
Future<bool> setEnableAEC(bool enable) async {
return await _channel.invokeMethod('setEnableAEC', {
'enable': enable,
}) ?? false;
}
/// 下载离线资源
///
/// [voiceTypes] 音色类型列表
/// [languages] 语言列表
Future<bool> downloadOfflineResource({
List<String> voiceTypes = const ['qingxin'],
List<String> languages = const ['zh-CN'],
}) async {
return await _channel.invokeMethod('downloadOfflineResource', {
'voiceTypes': voiceTypes,
'languages': languages,
}) ?? false;
}
/// 设置语音参数
///
/// [rate] 语速 -500~500
/// [volume] 音量 0~100
/// [pitch] 音调 -500~500
/// [silenceDuration] 静音时长,毫秒
Future<bool> setSpeechParams({
int rate = 0,
int volume = 100,
int pitch = 0,
int silenceDuration = 0,
}) async {
return await _channel.invokeMethod('setSpeechParams', {
'rate': rate,
'volume': volume,
'pitch': pitch,
'silenceDuration': silenceDuration,
}) ?? false;
}
/// 设置音频输出类型
///
/// [outputType] 输出类型,speaker(扬声器),earpiece(听筒),auto(自动)
Future<bool> setAudioOutputType(String outputType) async {
return await _channel.invokeMethod('setAudioOutputType', {
'outputType': outputType,
}) ?? false;
}
/// 合成并播放文本
///
/// [text] 待合成的文本
Future<bool> speakText(String text) async {
return await _channel.invokeMethod('speakText', {
'text': text,
}) ?? false;
}
/// 使用大模型合成并播放文本
///
/// [text] 待合成的文本
/// [voiceId] 声音ID
/// [params] 额外参数,JSON字符串
Future<bool> speakWithBigModel({
required String text,
required String voiceId,
String params = '',
}) async {
return await _channel.invokeMethod('speakWithBigModel', {
'text': text,
'voiceId': voiceId,
'params': params,
}) ?? false;
}
/// 暂停播放
Future<bool> pausePlayback() async {
return await _channel.invokeMethod('pausePlayback') ?? false;
}
/// 恢复播放
Future<bool> resumePlayback() async {
return await _channel.invokeMethod('resumePlayback') ?? false;
}
/// 停止播放
Future<bool> stopSpeaking() async {
return await _channel.invokeMethod('stopSpeaking') ?? false;
}
/// 释放TTS资源
Future<bool> dispose() async {
return await _channel.invokeMethod('dispose') ?? false;
}
}

28
local_plugins/volcano_speech/pubspec.yaml

@ -0,0 +1,28 @@
name: volcano_speech
description: 火山引擎语音合成服务插件
version: 0.0.1
homepage:
environment:
sdk: ">=2.17.0 <3.0.0"
flutter: ">=2.5.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
plugin:
platforms:
android:
package: com.yunqiinnovation.volcano_speech
pluginClass: VolcanoSpeechPlugin
ios:
pluginClass: VolcanoSpeechPlugin

2
pubspec.yaml

@ -64,6 +64,8 @@ dependencies:
path: local_plugins/azure_speech path: local_plugins/azure_speech
open_ai_service: open_ai_service:
path: local_plugins/open_ai_service path: local_plugins/open_ai_service
volcano_speech:
path: local_plugins/volcano_speech
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

Loading…
Cancel
Save