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