14 changed files with 2349 additions and 8 deletions
@ -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(); |
|||
}, |
|||
``` |
|||
@ -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") |
|||
} |
|||
@ -0,0 +1 @@ |
|||
rootProject.name = "volcano_speech" |
|||
@ -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> |
|||
@ -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) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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() |
|||
} |
|||
} |
|||
@ -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) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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) |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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 |
|||
Loading…
Reference in new issue