You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
603 lines
15 KiB
603 lines
15 KiB
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;
|
|
}
|
|
}
|