diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index b0a39cbd4..026060648 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -93,14 +93,13 @@ dependencies {
// 添加本地插件模块依赖
implementation(project(":azure_speech"))
implementation(project(":open_ai_service"))
+ implementation(project(":volcano_speech"))
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.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依赖
implementation("androidx.media:media:1.6.0")
@@ -111,11 +110,7 @@ dependencies {
// 添加 AndroidX Security 加密 SharedPreferences 依赖
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 {
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index f8f47c600..67f214248 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -3,6 +3,10 @@ allprojects {
google()
mavenCentral()
maven { url = uri("https://storage.googleapis.com/download.flutter.io") }
+ // 添加火山引擎Maven仓库
+ maven {
+ url = uri("https://artifact.bytedance.com/repository/Volcengine/")
+ }
}
}
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
index 6ca3ffd16..c0eebbf43 100644
--- a/android/settings.gradle.kts
+++ b/android/settings.gradle.kts
@@ -31,8 +31,9 @@ plugins {
include(":app")
include(":azure_speech")
include(":open_ai_service")
-
+include(":volcano_speech")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
+project(":volcano_speech").projectDir = file("../local_plugins/volcano_speech/android")
diff --git a/local_plugins/volcano_speech/README.md b/local_plugins/volcano_speech/README.md
new file mode 100644
index 000000000..572317b57
--- /dev/null
+++ b/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: '这是一段2023-10-01的语音合成'
+);
+
+// 设置情感
+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();
+},
+```
\ No newline at end of file
diff --git a/local_plugins/volcano_speech/android/build.gradle.kts b/local_plugins/volcano_speech/android/build.gradle.kts
new file mode 100644
index 000000000..86b03fd0e
--- /dev/null
+++ b/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 {
+ 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 {
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ // 添加火山引擎语音合成SDK
+ implementation("com.bytedance.speechengine:speechengine_tob:0.0.5")
+}
\ No newline at end of file
diff --git a/local_plugins/volcano_speech/android/settings.gradle.kts b/local_plugins/volcano_speech/android/settings.gradle.kts
new file mode 100644
index 000000000..7f5342866
--- /dev/null
+++ b/local_plugins/volcano_speech/android/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = "volcano_speech"
\ No newline at end of file
diff --git a/local_plugins/volcano_speech/android/src/main/AndroidManifest.xml b/local_plugins/volcano_speech/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..338e8293c
--- /dev/null
+++ b/local_plugins/volcano_speech/android/src/main/AndroidManifest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt
new file mode 100644
index 000000000..614474d1a
--- /dev/null
+++ b/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)
+ }
+ }
+ }
+ }
+}
diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt
new file mode 100644
index 000000000..bb6f1e452
--- /dev/null
+++ b/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) {
+ 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) {
+ 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("appId") ?: ""
+ val apiKey = call.argument("apiKey") ?: ""
+ val resourceId = call.argument("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("language") ?: "zh-CN"
+ result.success(volcanoAsrHelper.setLanguage(language))
+ }
+ "setHotWords" -> {
+ val hotWordsId = call.argument("hotWordsId") ?: ""
+ result.success(volcanoAsrHelper.setHotWords(hotWordsId))
+ }
+ "setVadParams" -> {
+ val forceToSpeechTime = call.argument("forceToSpeechTime") ?: 0
+ val endWindowSize = call.argument("endWindowSize") ?: 800
+ result.success(volcanoAsrHelper.setVadParams(forceToSpeechTime, endWindowSize))
+ }
+ "setCorrectWords" -> {
+ val correctWordsJson = call.argument("correctWordsJson") ?: "{}"
+ result.success(volcanoAsrHelper.setCorrectWords(correctWordsJson))
+ }
+ "setEnableVolume" -> {
+ val enable = call.argument("enable") ?: false
+ result.success(volcanoAsrHelper.setEnableVolume(enable))
+ }
+ "setShowUtterances" -> {
+ val enable = call.argument("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("appId") ?: ""
+ val token = call.argument("token") ?: ""
+ val resourceId = call.argument("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("voice") ?: ""
+
+ if (voice.isEmpty()) {
+ result.error("INVALID_ARGUMENTS", "voice不能为空", null)
+ return
+ }
+
+ result.success(volcanoTtsHelper.setVoice(voice))
+ }
+ "setContinuousMode" -> {
+ val isContinuous = call.argument("isContinuous") ?: false
+ result.success(volcanoTtsHelper.setContinuousMode(isContinuous))
+ }
+ "speak" -> {
+ val text = call.argument("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("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("voiceName") ?: ""
+
+ if (voiceName.isEmpty()) {
+ result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
+ return
+ }
+
+ result.success(volcanoTtsHelper.setVoice(voiceName))
+ }
+ "speakText" -> {
+ val text = call.argument("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()
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt
new file mode 100644
index 000000000..e24a1ceb1
--- /dev/null
+++ b/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)
+ }
+ }
+ }
+ }
+}
diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/utils/FileLogger.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/utils/FileLogger.kt
new file mode 100644
index 000000000..8fdfb1227
--- /dev/null
+++ b/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)
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/volcano_speech/lib/volcano_speech.dart b/local_plugins/volcano_speech/lib/volcano_speech.dart
new file mode 100644
index 000000000..99da7d4b7
--- /dev/null
+++ b/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 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 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 _eventStreamController = StreamController.broadcast();
+
+ /// ASR事件流
+ Stream 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 getPlatformVersion() async {
+ return await _channel.invokeMethod('getPlatformVersion');
+ }
+
+ /// 初始化语音识别引擎
+ ///
+ /// [appId] 火山引擎AppID
+ /// [apiKey] 火山引擎ApiKey
+ /// [supportedLanguages] 支持的语言列表
+ /// [useBigModel] 是否使用大模型识别
+ /// [resourceId] 大模型资源ID(仅当useBigModel为true时有效)
+ Future initialize({
+ required String appId,
+ required String apiKey,
+ List 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 setLanguage(String language) async {
+ return await _channel.invokeMethod('setLanguage', {
+ 'language': language,
+ }) ?? false;
+ }
+
+ /// 设置热词
+ ///
+ /// [hotWords] 热词JSON字符串,例如 {"hotwords":[{"word":"快速入门","scale":2.0}]}
+ Future setHotWords(String hotWords) async {
+ return await _channel.invokeMethod('setHotWords', {
+ 'hotWords': hotWords,
+ }) ?? false;
+ }
+
+ /// 设置ASR请求参数
+ ///
+ /// [params] 请求参数JSON字符串
+ Future setRequestParams(String params) async {
+ return await _channel.invokeMethod('setRequestParams', {
+ 'params': params,
+ }) ?? false;
+ }
+
+ /// 启用语音停顿、分句、分词信息输出
+ ///
+ /// [enable] 是否启用
+ Future setShowUtterances(bool enable) async {
+ return await _channel.invokeMethod('setShowUtterances', {
+ 'enable': enable,
+ }) ?? false;
+ }
+
+ /// 一次性识别(直到说话结束)
+ ///
+ /// 返回识别结果文本和语言
+ Future