diff --git a/apps/client/GoogleService-Info.plist b/apps/client/GoogleService-Info.plist
deleted file mode 100644
index 05a2b5ce..00000000
--- a/apps/client/GoogleService-Info.plist
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
- API_KEY
- AIzaSyBEQPL-2Qh6fERxrxLP9Pzcm95mTmTpBxE
- GCM_SENDER_ID
- 733281995296
- PLIST_VERSION
- 1
- BUNDLE_ID
- com.yimai.eaimar
- PROJECT_ID
- voitrans
- STORAGE_BUCKET
- voitrans.firebasestorage.app
- IS_ADS_ENABLED
-
- IS_ANALYTICS_ENABLED
-
- IS_APPINVITE_ENABLED
-
- IS_GCM_ENABLED
-
- IS_SIGNIN_ENABLED
-
- GOOGLE_APP_ID
- 1:733281995296:ios:6a8351691312100bd70a45
-
-
\ No newline at end of file
diff --git a/apps/client/SpeexKit.xcframework/Info.plist b/apps/client/SpeexKit.xcframework/Info.plist
deleted file mode 100644
index c654fe5c..00000000
--- a/apps/client/SpeexKit.xcframework/Info.plist
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
- AvailableLibraries
-
-
- BinaryPath
- SpeexKit.framework/SpeexKit
- LibraryIdentifier
- ios-arm64
- LibraryPath
- SpeexKit.framework
- SupportedArchitectures
-
- arm64
-
- SupportedPlatform
- ios
-
-
- CFBundlePackageType
- XFWK
- XCFrameworkFormatVersion
- 1.0
-
-
diff --git a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/OpusTools.h b/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/OpusTools.h
deleted file mode 100644
index 55fa7f1e..00000000
--- a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/OpusTools.h
+++ /dev/null
@@ -1,44 +0,0 @@
-//
-// OpusTools.h
-// SpeexKit
-//
-// Created by EzioChan on 2024/1/17.
-// Copyright © 2024 www.zh-jieli.com. All rights reserved.
-//
-
-#import
-
-
-NS_ASSUME_NONNULL_BEGIN
-
-@protocol OpusToolsDelegate
-
--(void)opusToolsPcmData:(NSData*)data;
-
--(void)opusToolsPcmFile:(NSData *)data;
-
--(void)opusToolsOpusData:(NSData *)data;
-
--(void)opusToolsOpusFile:(NSData *)data;
-
-@end
-
-@interface OpusTools : NSObject
-
-@property(nonatomic,weak)id delegate;
-
--(void)opusDecodeInfo:(int)sampleRate Channel:(int)channel frameSize:(int) fs isHasHead:(BOOL)head;
-
--(void)opusEncodeInfo:(int)sampleRate Channel:(int)channel frameSize:(int)fs isHasHead:(BOOL)head;
-
--(void)decodeData:(NSData*)data;
-
--(void)decodeFile:(NSString*)path;
-
--(void)encodeData:(NSData*)data;
-
--(void)encodeFile:(NSString*)path;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/OpusUnit.h b/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/OpusUnit.h
deleted file mode 100644
index 37b12201..00000000
--- a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/OpusUnit.h
+++ /dev/null
@@ -1,100 +0,0 @@
-//
-// OpusUnit.h
-// QCY_Demo
-//
-// Created by 杰理科技 on 2021/7/5.
-// Copyright © 2021 杰理科技. All rights reserved.
-//
-
-#import
-
-NS_ASSUME_NONNULL_BEGIN
-
-/**
- * 通知:流式编码回调Opus数据
- * 通知数据类型:NSData(Opus)
- */
-extern NSString *kOPUS_ENCODE_DATA;
-/**
- * 通知:流式解码回调PCM数据
- * 通知数据类型:NSData(PCM)
- */
-extern NSString *kOPUS_DECODE_DATA;
-
-
-@interface OpusUnit : NSObject
-
-+ (void)opusIsLog:(BOOL)log;
-
-#pragma mark - Opus参数
-/**
- * 设置参数
- * @param rate 采样率 例如:48000、24000、16000、12000、8000(默认 16000)
- * @param frameRate 帧长度
- * 16k :40
- * 32K :80
- * 64k :160
- * 96k :320
- * 128k:640
- * @param channel 声道 例如:1、2 (默认 1)
- * @param isHeader 是否带头
- */
-+(void)opusSetSampleRate:(int)rate frameRate:(int)frameRate Channels:(int)channel isHeader:(BOOL)isHeader;
-
-#pragma mark - Opus解码
-
-/**
- * 直接【opus文件】转换成【pcm文件】
- * @param path_opus opus文件路径
- * @param path_pcm pcm文件路径
- */
-+ (int)opusDecodeOPUS:(NSString *)path_opus PCM:(NSString *)path_pcm;
-
-/**
- * 流式解码【开启】
- * 注意:需要异步调用,放在工程单利类中,切勿频繁开启。
- */
-+ (int)opusDecoderRun;
-
-/**
- * 输入Opus的数据
- * 通知监听“kOPUS_DECODE_DATA”获得解码后数据
- * 通知数据类型:NSNotification.object => NSData(PCM格式)
- * @param data opus数据流(长度:1024)
- */
-+ (void)opusWriteData:(NSData*)data;
-
-/**
- * 流式解码【关闭】
- */
-+ (int)opusDecoderStop;
-
-#pragma mark - Opus编码
-
-/**
- * 【pcm文件】转换成【opus文件】
- */
-+ (int)opusEncodePCM:(NSString *)path_pcm OPUS:(NSString *)path_opus;
-
-/**
- * 流式编码【开启】
- * 注意:需要异步调用,放在工程单利类中,切勿频繁开启。
- */
-+ (int)opusEncoderRun;
-
-/**
- * 输入pcm的数据
- * 通知监听“kOPUS_DECODE_DATA”获得解码后数据
- * 通知数据类型:NSNotification.object => NSData(opus格式)
- * @param data opus数据流(长度:1024)
- */
-+ (void)pcmWriteData:(NSData*)data;
-
-/**
- * 流式编码【关闭】
- */
-+ (int)opusEncoderStop;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/SpeexKit.h b/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/SpeexKit.h
deleted file mode 100644
index 0fb1c92b..00000000
--- a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/SpeexKit.h
+++ /dev/null
@@ -1,21 +0,0 @@
-//
-// SpeexKit.h
-// SpeexKit
-//
-// Created by zhihui liang on 2018/8/20.
-// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
-//
-
-#import
-
-//! Project version number for SpeexKit.
-FOUNDATION_EXPORT double SpeexKitVersionNumber;
-
-//! Project version string for SpeexKit.
-FOUNDATION_EXPORT const unsigned char SpeexKitVersionString[];
-
-// In this header, you should import all the public headers of your framework using statements like #import
-
-#import
-#import
-#import
diff --git a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/SpeexUnit.h b/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/SpeexUnit.h
deleted file mode 100644
index da7e93da..00000000
--- a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Headers/SpeexUnit.h
+++ /dev/null
@@ -1,40 +0,0 @@
-//
-// SpeexUnit.h
-// SpeexKit
-//
-// Created by zhihui liang on 2018/8/20.
-// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
-//
-
-#import
-
-/**
- * 流式解码回调PCM数据
- */
-extern NSString *kSPEEX_DECODE_DATA; //speex decode
-
-@interface SpeexUnit : NSObject
-
-+(void)speexIsLog:(BOOL)log;
-
-/**
- * 直接【speex文件】转换成【pcm文件】
- */
-+(int)speexDecodeSPX:(NSString *)path_spx PCM:(NSString *)path_pcm;
-
-/**
- * 流式解码【开启】
- */
-+(int)speexDecoderRun;
-
-/**
- * 输入Speex的数据
- */
-+(void)speexWriteData:(NSData*)data;
-
-/**
- * 流式解码【关闭】
- */
-+(int)speexDecoderStop;
-
-@end
diff --git a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Info.plist b/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Info.plist
deleted file mode 100644
index f1478bd9..00000000
Binary files a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Info.plist and /dev/null differ
diff --git a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Modules/module.modulemap b/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Modules/module.modulemap
deleted file mode 100644
index db8a3680..00000000
--- a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/Modules/module.modulemap
+++ /dev/null
@@ -1,6 +0,0 @@
-framework module SpeexKit {
- umbrella header "SpeexKit.h"
- export *
-
- module * { export * }
-}
diff --git a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/SpeexKit b/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/SpeexKit
deleted file mode 100644
index 194c091f..00000000
Binary files a/apps/client/SpeexKit.xcframework/ios-arm64/SpeexKit.framework/SpeexKit and /dev/null differ
diff --git a/apps/client/android/app/src/main/AndroidManifest.xml b/apps/client/android/app/src/main/AndroidManifest.xml
index 816b5935..b7c37f8b 100644
--- a/apps/client/android/app/src/main/AndroidManifest.xml
+++ b/apps/client/android/app/src/main/AndroidManifest.xml
@@ -112,8 +112,6 @@
android:name="androidx.work.impl.diagnostics.DiagnosticsReceiver"
tools:node="remove" />
-
-
- 3829022037361855
- fb3829022037361855
- 193d0a2b1757c41cfa557ea8e8f21c33
\ No newline at end of file
diff --git a/apps/client/android/build.gradle.kts b/apps/client/android/build.gradle.kts
index d86e79d1..6359ffd1 100644
--- a/apps/client/android/build.gradle.kts
+++ b/apps/client/android/build.gradle.kts
@@ -4,10 +4,6 @@ allprojects {
google()
mavenCentral()
maven { url = uri("https://storage.googleapis.com/download.flutter.io") }
- // 添加火山引擎Maven仓库
- maven {
- url = uri("https://artifact.bytedance.com/repository/Volcengine/")
- }
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") }
// 修改为绝对路径,避免相对路径计算错误
diff --git a/apps/client/android/settings.gradle.kts b/apps/client/android/settings.gradle.kts
index 7984d0cc..2571d00c 100644
--- a/apps/client/android/settings.gradle.kts
+++ b/apps/client/android/settings.gradle.kts
@@ -39,7 +39,6 @@ include(":classic_bluetooth")
include(":agent_service")
include(":speech")
include(":chat_api")
-include(":bytedance_speech")
include(":qq_music") // 添加这行
include(":bluetooth_manager") // 恒玄(BES)经典蓝牙连接 + G722/mSBC 音频编解码
@@ -51,6 +50,5 @@ project(":classic_bluetooth").projectDir = file("../local_plugins/classic_blueto
project(":agent_service").projectDir = file("../local_plugins/agent_service/android")
project(":speech").projectDir = file("../local_plugins/speech/android")
project(":chat_api").projectDir = file("../local_plugins/chat_api/android")
-project(":bytedance_speech").projectDir = file("../local_plugins/bytedance_speech/android")
project(":qq_music").projectDir = file("../local_plugins/qq_music/android") // 添加这行
project(":bluetooth_manager").projectDir = file("../local_plugins/bluetooth_manager/android")
diff --git a/apps/client/assets/config/GoogleService-Info.plist b/apps/client/assets/config/GoogleService-Info.plist
deleted file mode 100644
index 05a2b5ce..00000000
--- a/apps/client/assets/config/GoogleService-Info.plist
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
- API_KEY
- AIzaSyBEQPL-2Qh6fERxrxLP9Pzcm95mTmTpBxE
- GCM_SENDER_ID
- 733281995296
- PLIST_VERSION
- 1
- BUNDLE_ID
- com.yimai.eaimar
- PROJECT_ID
- voitrans
- STORAGE_BUCKET
- voitrans.firebasestorage.app
- IS_ADS_ENABLED
-
- IS_ANALYTICS_ENABLED
-
- IS_APPINVITE_ENABLED
-
- IS_GCM_ENABLED
-
- IS_SIGNIN_ENABLED
-
- GOOGLE_APP_ID
- 1:733281995296:ios:6a8351691312100bd70a45
-
-
\ No newline at end of file
diff --git a/apps/client/ios/Runner/Info.plist b/apps/client/ios/Runner/Info.plist
index f4cb5025..980c66a8 100644
--- a/apps/client/ios/Runner/Info.plist
+++ b/apps/client/ios/Runner/Info.plist
@@ -24,12 +24,6 @@
????
CFBundleURLTypes
-
- CFBundleURLSchemes
-
- fb3829022037361855
-
-
CFBundleTypeRole
Editor
@@ -65,14 +59,6 @@
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
- FacebookAdvertiserIDCollectionEnabled
-
- FacebookAppID
- 3829022037361855
- FacebookAutoLogAppEventsEnabled
-
- FacebookClientToken
- 193d0a2b1757c41cfa557ea8e8f21c33
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
@@ -83,21 +69,6 @@
weixin
weixinULAPI
weixinURLParamsAPI
- fbapi
- fbapi20130214
- fbapi20130410
- fbapi20130702
- fbapi20131010
- fbapi20131219
- fbapi20140410
- fbapi20140116
- fbapi20150313
- fbapi20150629
- fbapi20160328
- fbauth
- fb-messenger-share-api
- fbauth2
- fbshareextension
google.navigation
googlemaps
comgooglemaps
@@ -170,8 +141,6 @@
Location access enables the app to provide location-based voice translation services, such as translating local place names and providing contextual information relevant to your current location during conversations.
NSMicrophoneUsageDescription
Our app's voice AI interaction features (such as voice wake-up for AI, issuing commands, and real-time conversations) require microphone access. With this permission, you can interact with AI directly through speech, improving operational efficiency. We strictly protect your voice data—only used for real-time processing of interaction requests, and raw audio will not be stored.
- NSMotionUsageDescription
- This app uses motion and fitness data to detect when you're walking or moving, which helps optimize voice recognition accuracy and automatically adjust microphone sensitivity for better audio recording quality during different activities.
NSPhotoLibraryAddUsageDescription
Photo library permission is required to save exported images (mind maps, statistics) to your album
NSPhotoLibraryUsageDescription
diff --git a/apps/client/local_plugins/agent_service/android/build.gradle.kts b/apps/client/local_plugins/agent_service/android/build.gradle.kts
index 958edf9f..fc1a06f6 100644
--- a/apps/client/local_plugins/agent_service/android/build.gradle.kts
+++ b/apps/client/local_plugins/agent_service/android/build.gradle.kts
@@ -51,7 +51,6 @@ dependencies {
add("compileOnly", project(":speech"))
add("compileOnly", project(":chat_api"))
add("compileOnly", project(":location_service"))
- add("compileOnly", project(":bytedance_speech"))
add("compileOnly", project(":user_portrait"))
add("compileOnly", project(":qq_music"))
diff --git a/apps/client/local_plugins/agent_service/ios/agent_service/Package.swift b/apps/client/local_plugins/agent_service/ios/agent_service/Package.swift
index 413e98de..e2038122 100644
--- a/apps/client/local_plugins/agent_service/ios/agent_service/Package.swift
+++ b/apps/client/local_plugins/agent_service/ios/agent_service/Package.swift
@@ -8,7 +8,6 @@ let package = Package(
.library(name: "agent-service", targets: ["agent_service"])
],
dependencies: [
- .package(path: "../../../test/ios/test"),
.package(path: "../../../ble_service/ios/ble_service"),
.package(path: "../../../speech/ios/speech"),
.package(path: "../../../azure_speech/ios/azure_speech"),
@@ -23,7 +22,6 @@ let package = Package(
.target(
name: "agent_service",
dependencies: [
- .product(name: "test", package: "test"),
.product(name: "ble-service", package: "ble_service"),
.product(name: "speech", package: "speech"),
.product(name: "azure-speech", package: "azure_speech"),
diff --git a/apps/client/local_plugins/agent_service/lib/agent_service.dart b/apps/client/local_plugins/agent_service/lib/agent_service.dart
index 06c1c1dd..801258e9 100644
--- a/apps/client/local_plugins/agent_service/lib/agent_service.dart
+++ b/apps/client/local_plugins/agent_service/lib/agent_service.dart
@@ -218,65 +218,6 @@ class AgentService {
}
}
- /// 初始化代理服务
- ///
- /// [config] 初始化配置,应包含以下参数:
- /// - azureSpeechKey: Azure语音服务密钥
- /// - azureSpeechRegion: Azure语音服务区域
- /// - openaiApiKey: OpenAI API密钥
- /// - openaiBaseUrl: (可选) OpenAI API 基础URL
- /// - openaiModel: (可选) OpenAI模型名称,默认为"gpt-3.5-turbo"
- /// - mcpServer: (可选) MCP服务器地址
- /// - systemPrompt: (可选) 自定义系统提示词,用于设置AI语音助手的行为和风格
- ///
- /// 返回是否初始化成功
- static Future initialize(Map config) async {
- try {
- final bool result = await _channel.invokeMethod('initialize', {
- 'config': config,
- });
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '初始化失败', e.details);
- }
- }
-
- /// 启动语音助手服务
- ///
- /// 启动常驻的语音助手服务,支持后台蓝牙设备唤醒和媒体按钮唤醒
- /// [config] 配置参数,应包含以下参数:
- /// - azureSpeechKey: Azure语音服务密钥
- /// - azureSpeechRegion: Azure语音服务区域
- /// - openaiApiKey: OpenAI API密钥
- /// - openaiBaseUrl: (可选) OpenAI API 基础URL
- /// - openaiModel: (可选) OpenAI模型名称,默认为"gpt-3.5-turbo"
- /// - mcpServer: (可选) MCP服务器地址
- ///
- /// 返回是否成功启动服务
- static Future startAgentService(Map config) async {
- try {
- final bool result = await _channel.invokeMethod('startAgentService', {
- 'config': config,
- });
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '启动语音助手服务失败', e.details);
- }
- }
-
- /// 停止语音助手服务
- ///
- /// 停止常驻的语音助手服务
- /// 返回是否成功停止服务
- static Future stopAgentService() async {
- try {
- final bool result = await _channel.invokeMethod('stopAgentService');
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '停止语音助手服务失败', e.details);
- }
- }
-
/// 开始对话
///
/// 启动语音识别,开始监听用户语音输入
@@ -317,22 +258,6 @@ class AgentService {
}
}
- /// 推送外部 PCM 音频数据到识别引擎
- ///
- /// 仅在通过 [startConversation] 设置 `isExternalActive = true` 时有效。
- /// PCM 格式要求:16bit signed, little-endian, 1 channel, 16000 Hz。
- static Future pushAudioData(Uint8List data) async {
- if (data.isEmpty) return false;
- try {
- final bool result = await _channel.invokeMethod('pushAudioData', {
- 'data': data,
- });
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '推送音频数据失败', e.details);
- }
- }
-
static Future isInterrupt(bool isInterrupt) async {
try {
final bool result = await _channel.invokeMethod('isInterrupt', {
@@ -344,46 +269,6 @@ class AgentService {
}
}
- /// 设备识别器
- ///
- /// 启动语音识别,开始监听用户语音输入
- /// 返回是否成功开始对话
- static Future recognizeCallback() async {
- try {
- final bool result = await _channel.invokeMethod('recognizeCallback');
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '设置识别器失败', e.details);
- }
- }
-
- /// 恢复原始音频状态
- ///
- /// 恢复原始音频状态
- /// 返回是否成功恢复原始音频状态
- static Future restoreOriginalAudioState() async {
- try {
- final bool result =
- await _channel.invokeMethod('restoreOriginalAudioState');
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '恢复原始音频状态失败', e.details);
- }
- }
-
- /// 禁用蓝牙音频
- ///
- /// 禁用蓝牙音频
- /// 返回是否成功禁用蓝牙音频
- static Future disableBluetoothAudio() async {
- try {
- final bool result = await _channel.invokeMethod('disableBluetoothAudio');
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '禁用蓝牙音频失败', e.details);
- }
- }
-
/// 停止对话
///
/// 停止语音识别
@@ -428,71 +313,6 @@ class AgentService {
// }
// }
- /// 处理文本输入
- ///
- /// [text] 文本内容
- /// [speakResponse] 是否朗读响应
- /// 返回是否成功处理文本
- static Future processTextInput(String sessionid, String text,
- {bool speakResponse = false}) async {
- try {
- final bool result = await _channel.invokeMethod('processTextInput', {
- 'sessionid': sessionid,
- 'text': text,
- 'speakResponse': speakResponse,
- });
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '处理文本输入失败', e.details);
- }
- }
-
- /// 朗读文本
- ///
- /// [text] 要朗读的文本
- static Future speakText(String text) async {
- try {
- final bool result = await _channel.invokeMethod('speakText', {
- 'text': text,
- });
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '语音合成失败', e.details);
- }
- }
-
- /// 设置TTS语速
- static Future setTtsRate(int rate) async {
- try {
- final bool result = await _channel.invokeMethod('setTtsRate', {
- 'rate': rate,
- });
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '设置TTS语速失败', e.details);
- }
- }
-
- /// 停止语音合成
- static Future stopAiSteam() async {
- try {
- final bool result = await _channel.invokeMethod('stopAiSteam');
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '停止语音合成失败', e.details);
- }
- }
-
- /// 停止语音合成
- static Future stopTts() async {
- try {
- final bool result = await _channel.invokeMethod('stopTts');
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '停止语音合成失败', e.details);
- }
- }
-
/// 清除聊天历史
static Future clearChatHistory() async {
try {
@@ -547,30 +367,6 @@ class AgentService {
}
}
- /// 处理图片输入
- ///
- /// [imagePath] 图片文件路径
- /// [text] 可选的文本描述或问题
- /// [imageUrl] 图片url
- /// [speakResponse] 是否朗读响应
- /// 返回是否成功处理图片
- static Future processImageInput(
- String sessionid, String imagePath, String imageUrl,
- {String text = "", bool speakResponse = false}) async {
- try {
- final bool result = await _channel.invokeMethod('processImageInput', {
- 'sessionid': sessionid,
- 'imagePath': imagePath,
- 'imageUrl': imageUrl,
- 'text': text,
- 'speakResponse': speakResponse,
- });
- return result;
- } on PlatformException catch (e) {
- throw AgentServiceException(e.code, e.message ?? '处理图片输入失败', e.details);
- }
- }
-
/// 设置支持的语音识别语言
///
/// [languages] 支持的语言列表,如 ["zh-CN", "en-US"]
diff --git a/apps/client/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/apps/client/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
index 667ada40..0ec7ce8b 100644
--- a/apps/client/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
+++ b/apps/client/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
@@ -138,13 +138,6 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
}
}
- // 翻译(Volcano)相关
- private lateinit var translationChannel: MethodChannel
- private lateinit var translationEventChannel: EventChannel
- private var translationEventSink: EventChannel.EventSink? = null
- private var microsoftTranslationService: MicrosoftTranslationServiceImpl? = null
- private var microsoftTranslationAndTtsService: MicrosoftTranslationAndTtsService? = null
- private var translationRestProvider: String = "microsoft"
// 双向 AST 服务:A 和 B
private var azureAstHelperA: IntegratedSpeechTranslationService? = null
@@ -497,26 +490,6 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
}
// 翻译事件发送方法
- /**
- * 发送翻译事件到 Flutter 层
- * 参数通过 `Map` 传递,包含:
- * - type: 事件类型,如 `translationStarted`、`translated`、`translationFailed`
- * - 其他上下文字段:`text`、`originalText`、`translatedText`、`sourceLanguage`、`targetLanguage`、`error` 等
- */
- private fun sendTranslationEvent(event: Map) {
- if (translationEventSink == null) {
- FileLogger.w(tag, "无法发送翻译事件:事件通道未准备好")
- return
- }
-
- mainHandler.post {
- try {
- translationEventSink?.success(event)
- } catch (e: Exception) {
- FileLogger.e(tag, "发送翻译事件失败: ${e.message}")
- }
- }
- }
@@ -550,11 +523,6 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
astChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/ast")
astChannel.setMethodCallHandler(AsTMethodHandler())
- // 初始化翻译通道
- translationChannel =
- MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/translation")
- translationChannel.setMethodCallHandler(TranslationMethodHandler())
-
// 初始化ASR事件通道
asrEventChannel =
EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr_events")
@@ -633,18 +601,6 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
handleBailianDialogCall(call, result)
}
- // 初始化翻译事件通道
- translationEventChannel =
- EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/translation_events")
- translationEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
- override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
- translationEventSink = events
- }
-
- override fun onCancel(arguments: Any?) {
- translationEventSink = null
- }
- })
// 初始化音频数据事件通道
audioDataEventChannel =
EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/audio_data_events")
@@ -691,7 +647,6 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
doubaoAstHelperB = DoubaoE2ETranslateHelper(context)
bailianAstHelperA = AliyunBailianE2EHelper(context)
bailianAstHelperB = AliyunBailianE2EHelper(context)
- microsoftTranslationService = MicrosoftTranslationServiceImpl()
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
@@ -1945,263 +1900,14 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
}
// 翻译方法处理器(Volcano/Microsoft REST)
- /**
- * 翻译方法通道处理器
- * 提供:
- * - initialize(accessKey, secretKey, region, maxRetryAttempts, timeout)
- * - translateText(text, sourceLanguage, targetLanguage)
- * - dispose()
- */
- inner class TranslationMethodHandler : MethodCallHandler {
- override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
- when (call.method) {
- "initialize" -> {
- /**
- * 初始化翻译服务
- * provider: volcano/microsoft(默认 microsoft)
- * 语音翻译已统一为「千问端到端 + 微软三段式」,
- * REST 机器翻译的默认提供商随之从 volcano 改为 microsoft。
- * volcano入参:`accessKey`、`secretKey`、`region`、`maxRetryAttempts`、`timeout`
- * microsoft入参:`subscriptionKey`/`accessKey`、`region`/`location`、`endpoint?`、`maxRetryAttempts?`、`timeout?`
- */
- val provider = (call.argument("provider") ?: "microsoft").lowercase()
- translationRestProvider = provider
-
- try {
- // 火山已下线,REST 机器翻译统一走微软。
- // 这里刻意不再按 provider 分支——老客户端仍可能传 provider=volcano,
- // 分支删掉后若不兜底会走不到 result.success,Dart 侧永久挂起。
- run {
- val subscriptionKey = call.argument("subscriptionKey")
- ?: call.argument("accessKey")
- ?: ""
- val location = call.argument("location")
- val region = call.argument("region") ?: (location ?: "")
- val endpoint = call.argument("endpoint")
- ?: "https://api.cognitive.microsofttranslator.com"
- val maxRetryAttempts =
- call.argument("maxRetryAttempts")?.toInt() ?: 3
- val timeout = call.argument("timeout")?.toLong() ?: 10000L
-
- val svc = microsoftTranslationService
- ?: MicrosoftTranslationServiceImpl().also {
- microsoftTranslationService = it
- }
- kotlinx.coroutines.GlobalScope.launch(kotlinx.coroutines.Dispatchers.IO) {
- val success = svc.initialize(
- mapOf(
- "subscriptionKey" to subscriptionKey,
- "region" to region,
- "endpoint" to endpoint,
- "maxRetryAttempts" to maxRetryAttempts.toString(),
- "timeout" to timeout.toString()
- )
- )
- mainHandler.post { result.success(success) }
- }
- }
- } catch (e: Exception) {
- result.error("TRANSLATION_INIT_ERROR", e.message, null)
- }
- }
-
- "translateText" -> {
- /**
- * 文本翻译
- * 入参:`text` 源文本、`sourceLanguage` 源语言、`targetLanguage` 目标语言
- * 流程:先发送 `translationStarted` 事件,完成后发送 `translated` 或 `translationFailed`
- */
- val text = call.argument("text") ?: return result.error(
- "INVALID_ARGUMENTS",
- "text不能为空",
- null
- )
- val sourceLanguage = call.argument("sourceLanguage")
- ?: return result.error("INVALID_ARGUMENTS", "sourceLanguage不能为空", null)
- val targetLanguage = call.argument("targetLanguage")
- ?: return result.error("INVALID_ARGUMENTS", "targetLanguage不能为空", null)
- Log.d(
- tag,
- "translateText: text=$text, sourceLanguage=$sourceLanguage, targetLanguage=$targetLanguage"
- )
- if (translationEventSink == null) {
- return result.error(
- "EVENT_CHANNEL_NOT_READY",
- "事件通道未准备好,无法执行翻译",
- null
- )
- }
- Log.d(tag, "translateText: 发送 translationStarted 事件")
- val provider =
- (call.argument("provider") ?: translationRestProvider).lowercase()
- Log.d(tag, "translateText: provider=$provider")
- val svc = microsoftTranslationService
- ?: MicrosoftTranslationServiceImpl().also {
- microsoftTranslationService = it
- }
- microsoftTranslationService ?: MicrosoftTranslationServiceImpl().also {
- microsoftTranslationService = it
- }
- sendTranslationEvent(
- mapOf(
- "type" to "translationStarted",
- "text" to text,
- "sourceLanguage" to sourceLanguage,
- "targetLanguage" to targetLanguage
- )
- )
-
- kotlinx.coroutines.GlobalScope.launch(kotlinx.coroutines.Dispatchers.IO) {
- val res = svc.translateText(text, sourceLanguage, targetLanguage)
- val event = if (res.success) {
- mapOf(
- "type" to "translated",
- "originalText" to text,
- "translatedText" to (res.translatedText ?: ""),
- "targetLanguage" to targetLanguage
- )
- } else {
- mapOf(
- "type" to "translationFailed",
- "text" to text,
- "error" to (res.error ?: "未知错误")
- )
- }
- sendTranslationEvent(event)
- mainHandler.post { result.success(res.success) }
- }
- }
-
- "initialize1" -> {
- val speechSubscriptionKey = call.argument("speechSubscriptionKey")
- ?: call.argument("subscriptionKey")
- ?: ""
- val speechRegion = call.argument("speechRegion")
- ?: call.argument("region")
- ?: ""
-
- val translationSubscriptionKey =
- call.argument("translationSubscriptionKey")
- ?: speechSubscriptionKey
- val translationRegion = call.argument("translationRegion")
- ?: speechRegion
-
- FileLogger.d(
- tag,
- "initialize1 called, speechRegion=$speechRegion, translationRegion=$translationRegion, " +
- "speechSubscriptionKey=${speechSubscriptionKey}, translationSubscriptionKey=${translationSubscriptionKey}"
- )
-
- val svc = microsoftTranslationAndTtsService
- ?: MicrosoftTranslationAndTtsService().also {
- microsoftTranslationAndTtsService = it
- }
-
- kotlinx.coroutines.GlobalScope.launch(kotlinx.coroutines.Dispatchers.IO) {
- val config = MicrosoftTranslationAndTtsService.ServiceConfig(
- speechSubscriptionKey = speechSubscriptionKey,
- speechRegion = speechRegion,
- translationSubscriptionKey = translationSubscriptionKey,
- translationRegion = translationRegion
- )
- val success = svc.initialize(config)
- mainHandler.post { result.success(success) }
- }
- }
-
- "process" -> {
- val text = call.argument("text") ?: return result.error(
- "INVALID_ARGUMENTS",
- "text不能为空",
- null
- )
- val sourceLanguage = call.argument("sourceLanguage")
- ?: return result.error("INVALID_ARGUMENTS", "sourceLanguage不能为空", null)
- val targetLanguage = call.argument("targetLanguage")
- ?: return result.error("INVALID_ARGUMENTS", "targetLanguage不能为空", null)
- val targetTTSLanguage = call.argument("targetTTSLanguage")
- ?: return result.error(
- "INVALID_ARGUMENTS",
- "targetTTSLanguage不能为空",
- null
- )
- FileLogger.d(
- tag,
- "process called, textLength=${text.length}, source=$sourceLanguage, target=$targetLanguage, targetTTSLanguage=$targetTTSLanguage"
- )
-
- val svc = microsoftTranslationAndTtsService
- if (svc == null) {
- return result.error("SERVICE_NOT_INITIALIZED", "请先调用 initialize", null)
- }
-
- // 使用应用files目录下的 tts_output 子目录
- val outputDir = java.io.File(context.filesDir, "tts_output")
-
- val cachedFile = java.io.File(outputDir, "$targetLanguage.wav")
- if (cachedFile.exists() && cachedFile.isFile) {
- FileLogger.d(tag, "process cache hit, filePath=${cachedFile.absolutePath}")
- result.success(cachedFile.absolutePath)
- return
- }
-
- kotlinx.coroutines.GlobalScope.launch(kotlinx.coroutines.Dispatchers.IO) {
- val filePath = svc.processAndSaveAudio(
- text,
- sourceLanguage,
- targetLanguage,
- targetTTSLanguage,
- outputDir
- )
- mainHandler.post {
- if (filePath != null) {
- FileLogger.d(tag, "process success, filePath=$filePath")
- result.success(filePath)
- } else {
- FileLogger.e(tag, "process failed, filePath is null")
- result.error("PROCESS_FAILED", "处理失败", null)
- }
- }
- }
- }
-
- "dispose1" -> {
- FileLogger.d(
- tag,
- "dispose1 called, releasing MicrosoftTranslationAndTtsService"
- )
- microsoftTranslationAndTtsService?.dispose()
- result.success(true)
- }
-
- "dispose" -> {
- /**
- * 释放翻译服务资源
- */
- try {
- microsoftTranslationService?.dispose()
- result.success(true)
- } catch (e: Exception) {
- result.error("TRANSLATION_DISPOSE_ERROR", e.message, null)
- }
- }
-
- else -> result.notImplemented()
- }
- }
- }
-
-
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
asrChannel.setMethodCallHandler(null)
ttsChannel.setMethodCallHandler(null)
astChannel.setMethodCallHandler(null)
- translationChannel.setMethodCallHandler(null)
asrEventChannel.setStreamHandler(null)
ttsEventChannel.setStreamHandler(null)
astEventChannel.setStreamHandler(null)
if (::astTtsEventChannel.isInitialized) astTtsEventChannel.setStreamHandler(null)
- translationEventChannel.setStreamHandler(null)
try {
if (isRegisteredToBle) {
diff --git a/apps/client/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/MicrosoftTranslationAndTtsService.kt b/apps/client/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/MicrosoftTranslationAndTtsService.kt
deleted file mode 100644
index c1fcaec3..00000000
--- a/apps/client/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/MicrosoftTranslationAndTtsService.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-package com.yunqiinnovation.azure_speech
-
-import android.util.Log
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.io.FileOutputStream
-import java.util.UUID
-
-/**
- * 微软翻译与TTS综合服务
- *
- * 功能:
- * 1. 接收输入文本、源语言代码、目标语言代码
- * 2. 调用微软翻译服务将文本翻译为目标语言
- * 3. 调用微软TTS服务将翻译后的文本合成为语音
- * 4. 将合成的音频保存为 WAV 文件,文件名为 "{targetLanguage}.wav"
- */
-class MicrosoftTranslationAndTtsService {
-
- companion object {
- private const val TAG = "MsTransAndTtsService"
- }
-
- private val translationService = MicrosoftTranslationServiceImpl()
- private val ttsService = MicrosoftTTSServiceImpl()
-
- private var serviceConfig: ServiceConfig? = null
-
- /**
- * 服务配置
- * @param speechSubscriptionKey Azure 语音服务订阅密钥(用于 TTS)
- * @param speechRegion Azure 语音服务区域 (例如 "eastus")
- * @param translationSubscriptionKey Azure 翻译服务订阅密钥(用于文本翻译)
- * @param translationRegion Azure 翻译服务区域
- */
- data class ServiceConfig(
- val speechSubscriptionKey: String,
- val speechRegion: String,
- val translationSubscriptionKey: String = speechSubscriptionKey,
- val translationRegion: String = speechRegion
- )
-
- /**
- * 初始化服务
- * @param config 服务配置
- */
- suspend fun initialize(config: ServiceConfig): Boolean {
- this.serviceConfig = config
-
- // 初始化翻译服务
- // 翻译服务支持的配置键: subscriptionKey, region
- val transConfig = mapOf(
- "subscriptionKey" to config.translationSubscriptionKey,
- "region" to config.translationRegion
- )
-
- Log.d(TAG, "正在初始化翻译服务...")
- val transInitSuccess = translationService.initialize(transConfig)
- if (!transInitSuccess) {
- Log.e(TAG, "翻译服务初始化失败")
- return false
- }
-
- Log.d(TAG, "服务初始化成功")
- return true
- }
-
- /**
- * 执行 翻译 -> TTS -> 保存文件 流程
- *
- * @param text 待翻译和合成的文本
- * @param sourceLanguage 源语言代码 (例如 "zh-CN")
- * @param targetLanguage 翻译目标语言代码 (例如 "en-US")
- * @param targetTTSLanguage 实际用于TTS的语言代码(可与翻译目标不同)
- * @param outputDirectory 音频文件保存目录
- * @return 生成的音频文件绝对路径,如果失败则返回 null
- */
- suspend fun processAndSaveAudio(
- text: String,
- sourceLanguage: String,
- targetLanguage: String,
- targetTTSLanguage: String,
- outputDirectory: File
- ): String? = withContext(Dispatchers.IO) {
- val config = serviceConfig
- if (config == null) {
- Log.e(TAG, "服务尚未初始化,请先调用 initialize()")
- return@withContext null
- }
-
- // --- 步骤 1: 文本翻译 ---
- Log.d(TAG, "开始翻译: text=$text, $sourceLanguage -> $targetLanguage")
- val translationResult = translationService.translateText(text, sourceLanguage, targetLanguage)
-
- if (!translationResult.success || translationResult.translatedText.isNullOrEmpty()) {
- Log.e(TAG, "翻译失败: ${translationResult.error}")
- return@withContext null
- }
-
- val translatedText = translationResult.translatedText!!
- Log.i(TAG, "翻译完成: $translatedText")
-
- // --- 步骤 2: TTS 合成 ---
- Log.d(TAG, "开始TTS合成...")
-
- // 根据上传的 TTS 语言代码优先配置 TTS,其次回退到翻译目标语言
- val ttsConfig = resolveTtsConfig(targetTTSLanguage, config)
-
- // 创建一个空的 Callback,因为我们主要依赖 synthesize 方法的返回值
- val ttsCallback = object : MicrosoftTTSServiceImpl.TtsCallback {
- override fun onSynthesisStarted(utteranceId: String, text: String) {}
- override fun onSynthesisProgress(utteranceId: String, text: String, progress: Float) {}
- override fun onAudioChunkGenerated(utteranceId: String, text: String, audioChunk: ByteArray) {}
- override fun onSynthesisCompleted(utteranceId: String, text: String, fullAudio: ByteArray?) {}
- override fun onSynthesisFailed(utteranceId: String, text: String, error: String) {
- Log.w(TAG, "TTS Callback 报告失败: $error")
- }
- override fun onError(message: String) {
- Log.w(TAG, "TTS Callback 报告错误: $message")
- }
- }
-
- val ttsInitSuccess = ttsService.initialize(ttsConfig, ttsCallback)
- if (!ttsInitSuccess) {
- Log.e(TAG, "TTS 服务初始化失败")
- return@withContext null
- }
-
- val utteranceId = UUID.randomUUID().toString()
- val audioData = ttsService.synthesize(utteranceId, translatedText)
-
- if (audioData == null || audioData.isEmpty()) {
- Log.e(TAG, "TTS 合成返回空数据")
- return@withContext null
- }
- Log.i(TAG, "TTS 合成成功,数据大小: ${audioData.size} bytes")
-
- // --- 步骤 3: 保存文件 ---
- try {
- if (!outputDirectory.exists()) {
- outputDirectory.mkdirs()
- }
-
- // 文件名为目标语言代码.wav
- val fileName = "$targetLanguage.wav"
- val outputFile = File(outputDirectory, fileName)
-
- FileOutputStream(outputFile).use { fos ->
- fos.write(audioData)
- fos.flush()
- }
-
- Log.i(TAG, "音频文件已保存: ${outputFile.absolutePath}")
- return@withContext outputFile.absolutePath
-
- } catch (e: Exception) {
- Log.e(TAG, "保存音频文件失败", e)
- return@withContext null
- }
- }
-
- /**
- * 根据目标语言构建 TTS 配置
- * @param targetTTSLanguage 翻译目标语言代码(如 en、en-US、zh-Hans)
- * @param config 服务配置(包含语音服务 key/region)
- */
- private fun resolveTtsConfig(
- targetTTSLanguage: String,
- config: ServiceConfig
- ): MicrosoftTTSServiceImpl.TtsConfig {
-
- return MicrosoftTTSServiceImpl.TtsConfig(
- subscriptionKey = config.speechSubscriptionKey,
- region = config.speechRegion,
- targetLanguage = targetTTSLanguage,
- voiceName = targetTTSLanguage,
- enableAudioPlayback = false
- )
- }
-
- /**
- * 释放资源
- */
- fun dispose() {
- translationService.dispose()
- ttsService.dispose()
- }
-}
diff --git a/apps/client/local_plugins/azure_speech/lib/main.dart b/apps/client/local_plugins/azure_speech/lib/main.dart
deleted file mode 100644
index a7256585..00000000
--- a/apps/client/local_plugins/azure_speech/lib/main.dart
+++ /dev/null
@@ -1,20 +0,0 @@
-import 'package:flutter/material.dart';
-
-void main() {
- runApp(const MainApp());
-}
-
-class MainApp extends StatelessWidget {
- const MainApp({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const MaterialApp(
- home: Scaffold(
- body: Center(
- child: Text('Hello World!'),
- ),
- ),
- );
- }
-}
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/Info.plist b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/Info.plist
deleted file mode 100644
index 21abfc01..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/Info.plist
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
- AvailableLibraries
-
-
- BinaryPath
- JLAudioUnitKit.framework/JLAudioUnitKit
- LibraryIdentifier
- ios-arm64
- LibraryPath
- JLAudioUnitKit.framework
- SupportedArchitectures
-
- arm64
-
- SupportedPlatform
- ios
-
-
- CFBundlePackageType
- XFWK
- XCFrameworkFormatVersion
- 1.0
-
-
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLAudioUnitKit.h b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLAudioUnitKit.h
deleted file mode 100644
index b7141b1b..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLAudioUnitKit.h
+++ /dev/null
@@ -1,24 +0,0 @@
-//
-// JLAudioUnitKit.h
-// JLAudioUnitKit
-//
-// Created by EzioChan on 2024/11/14.
-//
-
-#import
-
-//! Project version number for JLAudioUnitKit.
-FOUNDATION_EXPORT double JLAudioUnitKitVersionNumber;
-
-//! Project version string for JLAudioUnitKit.
-FOUNDATION_EXPORT const unsigned char JLAudioUnitKitVersionString[];
-
-// In this header, you should import all the public headers of your framework using statements like #import
-
-#import
-#import
-#import
-#import
-#import
-#import
-#import
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLAudioUnitPlayer.h b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLAudioUnitPlayer.h
deleted file mode 100644
index 7da831c6..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLAudioUnitPlayer.h
+++ /dev/null
@@ -1,58 +0,0 @@
-//
-// JLAudioUnitPlayer.h
-// JLAudioUnitKit
-//
-// Created by EzioChan on 2025/4/16.
-// Copyright © 2025 ZhuHai JieLi Technology Co.,Ltd. All rights reserved.
-//
-
-#import
-#import
-
-NS_ASSUME_NONNULL_BEGIN
-
-typedef NS_ENUM(NSInteger, JLAudioUnitPlayerType) {
- JLAudioUnitPlayerTypeFile, // 支持 MP3/WAV/AAC 等文件格式(AVAudioPlayer)
- JLAudioUnitPlayerTypePCM // 支持 PCM 流式播放(Audio Queue)
-};
-
-@class JLAudioUnitPlayer;
-
-@protocol JLAudioPlayerDelegate
-@optional
-// 播放进度更新(秒)
-- (void)audioPlayer:(JLAudioUnitPlayer *)player didUpdateProgress:(NSTimeInterval)currentTime duration:(NSTimeInterval)duration;
-// 播放完成
-- (void)audioPlayerDidFinishPlaying:(JLAudioUnitPlayer *)player;
-// 播放错误
-- (void)audioPlayer:(JLAudioUnitPlayer *)player didFailWithError:(NSError *)error;
-@end
-
-@interface JLAudioUnitPlayer : NSObject
-
-@property (nonatomic, weak) id delegate;
-@property (nonatomic, readonly) NSTimeInterval duration; // 总时长(文件模式下有效)
-@property (nonatomic, readonly) NSTimeInterval currentTime;
-@property (nonatomic, readonly) BOOL isPlaying;
-
-#pragma mark - 初始化方法
-
-/// 文件播放初始化(MP3/WAV/AAC)
-- (instancetype)initWithAudioFile:(NSString *)filePath;
-
-/// PCM 流播放初始化(需指定格式)
-- (instancetype)initWithPCMFormat:(AudioStreamBasicDescription)format;
-
-#pragma mark - 播放控制
-- (void)play;
-- (void)pause;
-- (void)stop;
-- (void)seekToTime:(NSTimeInterval)time; // 仅文件模式有效
-
-#pragma mark - PCM 流式输入(仅 PCM 模式有效)
-- (void)appendPCMData:(NSData *)pcmData;
-- (void)endPCMStream; // 结束 PCM 流输入
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLOpusDecoder.h b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLOpusDecoder.h
deleted file mode 100644
index 711e2e99..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLOpusDecoder.h
+++ /dev/null
@@ -1,66 +0,0 @@
-//
-// JLOpusDecoder.h
-// JLAudioUnitKit
-//
-// Created by EzioChan on 2024/11/14.
-// Copyright © 2024 ZhuHai JieLi Technology Co.,Ltd. All rights reserved.
-//
-
-#import
-#import
-
-NS_ASSUME_NONNULL_BEGIN
-@class JLOpusDecoder;
-
-/// Opus 解码代理
-@protocol JLOpusDecoderDelegate
-
-/// Opus 数据解码
-/// - Parameters:
-/// - decoder: 解码器
-/// - data: pcm 数据
-/// - error: 错误信息
--(void)opusDecoder:(JLOpusDecoder *)decoder Data:(NSData* _Nullable)data error:(NSError* _Nullable)error;
-
-@end
-
-typedef void(^JLOpusDecoderConvertBlock)(NSString *_Nullable pcmPath,NSError *_Nullable error);
-
-/// Opus 解码
-@interface JLOpusDecoder : NSObject
-
-/// 数据格式参数
-@property (nonatomic, strong) JLOpusFormat *opusFormat;
-
-/// 代理委托
-@property(nonatomic, weak) id delegate;
-
-- (instancetype)init NS_UNAVAILABLE;
-
-/// 初始化
-/// - Parameters:
-/// - format: 解码格式
-/// - delegate: 代理
-- (instancetype)initDecoder:(JLOpusFormat *)format delegate:(id)delegate;
-
-/// 重置解码格式
-/// - Parameter format: 解码格式
--(void)resetOpusFramet:(JLOpusFormat *)format;
-
-/// 输入 Opus 数据
-/// - Parameter data: Opus 数据
--(void)opusDecoderInputData:(NSData *)data;
-
-/// 解码文件
-/// - Parameters:
-/// - input: opus 文件
-/// - outPut: 输出路径
-/// - result: 结果回调
--(void)opusDecodeFile:(NSString *)input outPut:(NSString *_Nullable)outPut Resoult:(JLOpusDecoderConvertBlock _Nullable)result;
-
-/// 释放
--(void)opusOnRelease;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLOpusEncoder.h b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLOpusEncoder.h
deleted file mode 100644
index ef823053..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLOpusEncoder.h
+++ /dev/null
@@ -1,62 +0,0 @@
-//
-// JLOpusEncoder.h
-// JLAudioUnitKit
-//
-// Created by EzioChan on 2024/11/14.
-// Copyright © 2024 ZhuHai JieLi Technology Co.,Ltd. All rights reserved.
-//
-
-#import
-#import
-
-NS_ASSUME_NONNULL_BEGIN
-@class JLOpusEncoder;
-
-/// Opus 编码代理
-@protocol JLOpusEncoderDelegate
-
-/// PCM 数据编码
-/// - Parameters:
-/// - encoder: 解码器
-/// - data: opus 数据
-/// - error: 错误信息
--(void)opusEncoder:(JLOpusEncoder *)encoder Data:(NSData* _Nullable)data error:(NSError* _Nullable)error;
-
-@end
-
-typedef void(^JLOpusEncoderConvertBlock)(NSString *_Nullable pcmPath,NSError *_Nullable error);
-
-/// Opus 编码
-@interface JLOpusEncoder : NSObject
-
-/// 音频格式
-@property (nonatomic, strong) JLOpusFormat *opusFormat;
-
-/// 代理
-@property (nonatomic, weak) id delegate;
-
--(instancetype)init NS_UNAVAILABLE;
-
-/// 初始化
-/// - Parameters:
-/// - format: 音频格式
-/// - delegate: 代理
--(instancetype)initFormat:(JLOpusFormat *)format delegate:(id)delegate;
-
-/// PCM 数据
-/// - Parameter data: PCM 数据
--(void)opusEncodeData:(NSData *)data;
-
-/// PCM 文件转换成 Opus 文件
-/// - Parameters:
-/// - pcmPath: PCM 文件存放路径
-/// - outPut: 文件输出路径
-/// - result: 结果回调
--(void)opusEncodeFile:(NSString *)pcmPath outPut:(NSString *_Nullable)outPut Resoult:(JLOpusEncoderConvertBlock _Nullable)result;
-
-/// 释放
--(void)opusOnRelease;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLOpusFormat.h b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLOpusFormat.h
deleted file mode 100644
index 4b9b4418..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLOpusFormat.h
+++ /dev/null
@@ -1,53 +0,0 @@
-//
-// JLOpusOptions.h
-// JLAudioUnitKit
-//
-// Created by EzioChan on 2024/11/14.
-// Copyright © 2024 ZhuHai JieLi Technology Co.,Ltd. All rights reserved.
-//
-
-#import
-
-NS_ASSUME_NONNULL_BEGIN
-
-#define OPUS_JL_MAX_FRAME_SIZE 48000 * 2
-#define OPUS_JL_MAX_PACKET_SIZE 1500
-
-
-@interface JLOpusFormat : NSObject
-
-/// 采样率
-@property (nonatomic, assign) int sampleRate;
-
-/// 单/双声道
-@property (nonatomic, assign) int channels;
-
-/// 帧长度 20ms 默认值
-@property (nonatomic, assign) int frameDuration;
-
-/// BitRate
-@property (nonatomic, assign) int bitRate;
-
-/// 数据帧大小
-@property (nonatomic, assign, readonly) int frameSize;
-
-/// 数据大小
-@property (nonatomic, assign) int dataSize;
-
-/// 是否包含数据头部
-@property (nonatomic, assign) BOOL hasDataHeader;
-
--(instancetype)init NS_UNAVAILABLE;
-
-/// 默认配置
-/// sampleRate: 16000
-/// channels: 1
-/// frameDuration: 20
-/// dataSize: 40
-/// frameSize: 320
-/// hasDataHeader: YES
-+(JLOpusFormat*)defaultFormats;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLPcmToWav.h b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLPcmToWav.h
deleted file mode 100644
index b32ecef2..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLPcmToWav.h
+++ /dev/null
@@ -1,55 +0,0 @@
-//
-// JLPcmToWav.h
-// JLAudioUnitKit
-//
-// Created by EzioChan on 2025/4/15.
-// Copyright © 2025 ZhuHai JieLi Technology Co.,Ltd. All rights reserved.
-//
-
-#import
-
-NS_ASSUME_NONNULL_BEGIN
-
-/// PCM 转 WAV
-@interface JLPcmToWav : NSObject
-
-#pragma mark - 流式编码接口
-
-/// 初始化流式编码器
-/// @param outputPath 输出文件路径
-/// @param sampleRate 采样率(如 16000)
-/// @param numChannels 声道数(1 或 2)
-/// @param bitsPerSample 位深(16)
-- (instancetype)initWithOutputPath:(NSString *)outputPath
- sampleRate:(uint32_t)sampleRate
- numChannels:(uint16_t)numChannels
- bitsPerSample:(uint16_t)bitsPerSample;
-
-/// 追加 PCM 数据(需确保数据格式与初始化参数一致)
-/// @param pcmData PCM 数据块
-/// @param error 错误信息
-- (BOOL)appendPCMData:(NSData *)pcmData error:(NSError **)error;
-
-/// 完成编码并关闭文件(必须调用以更新 WAV 头)
-/// @param error 错误信息
-- (BOOL)finishWithError:(NSError **)error;
-
-#pragma mark - 一次性编码接口(便捷方法)
-
-/// 将 PCM 数据一次性转为 WAV 文件
-/// @param pcmData PCM 数据块
-/// @param outputPath 输出文件路径
-/// @param sampleRate 采样率(如 16000)
-/// @param numChannels 声道数(1 或 2)
-/// @param bitsPerSample 位深(16)
-/// @param error 错误信息
-+ (BOOL)convertPCMData:(NSData *)pcmData
- toWAVFile:(NSString *)outputPath
- sampleRate:(uint32_t)sampleRate
- numChannels:(uint16_t)numChannels
- bitsPerSample:(uint16_t)bitsPerSample
- error:(NSError **)error;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLPcmToWtg.h b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLPcmToWtg.h
deleted file mode 100644
index 231b3e83..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLPcmToWtg.h
+++ /dev/null
@@ -1,51 +0,0 @@
-//
-// JLPcmToWtg.h
-// JLAudioUnitKit
-//
-// Created by EzioChan on 2025/1/21.
-// Copyright © 2025 ZhuHai JieLi Technology Co.,Ltd. All rights reserved.
-//
-
-#import
-
-NS_ASSUME_NONNULL_BEGIN
-
-@interface JLPcm2WtgModel : NSObject
-
-/// 音频文件
-@property (nonatomic, copy) NSString *pcmPath;
-
-/// 输出文件
-@property (nonatomic, copy) NSString *wtgPath;
-
-@end
-
-
-/// JLPcmToWtgProtocol
-@protocol JLPcmToWtgDelegate
-
-/// 音频转码完成
-/// - Parameter model: 音频转码模型
-- (void)convertPcmToWtgDone:(JLPcm2WtgModel *)model;
-
-@end
-
-/// 音频转码
-/// PCM 转 WTG
-@interface JLPcmToWtg : NSObject
-
-- (instancetype)init NS_UNAVAILABLE;
-
-/// 音频转码初始化
-/// - Parameter delegate: 音频转码代理
-- (instancetype)initWithDelegate:(id)delegate;
-
-/// 音频转码
-/// 当前接口模型只限制 PCM 文件格式为:8k 16bit
-/// 输入的pcm文件(16bit,小端,8k采样率),wtg_filename:输出的wtg文件
-/// - Parameter model: 音频转码模型
--(void)convertPcmToWtg:(JLPcm2WtgModel *)model;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLSpeexDecoder.h b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLSpeexDecoder.h
deleted file mode 100644
index ad223d08..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Headers/JLSpeexDecoder.h
+++ /dev/null
@@ -1,54 +0,0 @@
-//
-// JLSpeexUnit.h
-// JLAudioUnitKit
-//
-// Created by EzioChan on 2024/11/14.
-//
-
-#import
-
-NS_ASSUME_NONNULL_BEGIN
-@class JLSpeexDecoder;
-
-@protocol JLSpeexDelegate
-
-/// Speex 数据解码
-/// - Parameters:
-/// - decoder: 解码器
-/// - data: PCM 数据
-/// - error: 错误信息
-- (void)speexDecoder:(JLSpeexDecoder *)decoder Data:(NSData* _Nullable)data error:(NSError* _Nullable)error;
-
-@end
-
-typedef void(^JLSpeexConvertBlock)(NSString *_Nullable pcmPath,NSError *_Nullable error);
-
-/// Speex 数据解码
-@interface JLSpeexDecoder : NSObject
-
--(instancetype)init NS_UNAVAILABLE;
-
-/// 初始化
-/// - Parameter delegate: 委托
--(instancetype)initWithDelegate:(id)delegate;
-
-/// 委托
-@property (nonatomic, weak) id delegate;
-
-/// 输入 Speex 数据
-/// - Parameter data: Speex 数据
--(void)speexInputData:(NSData*)data;
-
-/// 将 Speex 转换为 PCM
-/// - Parameters:
-/// - filePath: Speex 路径
-/// - opPath: 输出路径
-/// - result: 结果回调
--(void)speexConvertToPcm:(NSString *)filePath outPutFilePath:(NSString * _Nullable)opPath Result:(JLSpeexConvertBlock)result;
-
-/// 释放对象
--(void)speexOnRelease;
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Info.plist b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Info.plist
deleted file mode 100644
index 8d397ffb..00000000
Binary files a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Info.plist and /dev/null differ
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/JLAudioUnitKit b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/JLAudioUnitKit
deleted file mode 100755
index 4dc509fc..00000000
Binary files a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/JLAudioUnitKit and /dev/null differ
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Modules/module.modulemap b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Modules/module.modulemap
deleted file mode 100644
index 5e1ae1d3..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/Modules/module.modulemap
+++ /dev/null
@@ -1,6 +0,0 @@
-framework module JLAudioUnitKit {
- umbrella header "JLAudioUnitKit.h"
- export *
-
- module * { export * }
-}
diff --git a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/readme.txt b/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/readme.txt
deleted file mode 100644
index 6789fe6e..00000000
--- a/apps/client/local_plugins/ble_service/ios/ble_service/JLAudioUnitKit.xcframework/ios-arm64/JLAudioUnitKit.framework/readme.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-DESCRIPTION
------------
-This package includes the files needed to build the fixed point implementation
-of the ITU-T G.729 Appendix IV.
-
-DIFFERENCES WITH G.729B
------------------------
-The ANSI-C source code simulating the bit-exact, fixed point simulation
-software of enhanced VAD is integrated into the G.729B source code.
-The modification to the G.729B source code updated files of coder.c,
-cod_ld8k.c, bits.c, dec_sid.c, dtx.c, qsidgain.c, qsidlsf.c, tab_dtx.c,
-tab_dtx.h, ld8k.h, deleted vad.c/h and integrated the enhanced VAD functions
-into the G.729B project. The modification also replaced the original basic
-operators used in the original G.729B source code by the basic operators of
-ITU-T Software Tool Library STL2005.
-
-COMPILATION
------------
-For UNIX systems the following makefiles are provided
-
- coder.mak
- decoder.mak
-
-Edit the makefiles coder.mak and decoder.mak to set the proper options
-for your system.
-The command to compile and link all code on a UNIX system is
-
- make -f coder.mak
- make -f decoder.mak
-
-For other platforms, the *.mak files can be used to work out the
-compilation procedures.
-
-USAGE
------
-The command line instruction for the encoder is as follows:
- coder inputfile bitstreamfile dtx_option mode x
-where:
- inputfile : 8 kHz sampled data file containing 16 bit linear PCM signal
- bitstreamfile: binary file containing bitstream
- dtx_option : = 1 : DTX enabled 0 : DTX disabled
- mode X : Run encoder with enhanced VAD in operating point X,
- X=0 or 1 or 2 representing balanced or quality-preferred
- or bandwidth saving operating point, respectively
-
-The command line for the decoder is as follows:
- decoder bitstreamfile outputfile
-where:
- bitstreamfile: binary file containing bitstream
- outputfile : 8 kHz sampled data file containing 16 bit linear PCM signal
-
-
diff --git a/apps/client/local_plugins/bytedance_speech/README.md b/apps/client/local_plugins/bytedance_speech/README.md
deleted file mode 100644
index 1c81cc3f..00000000
--- a/apps/client/local_plugins/bytedance_speech/README.md
+++ /dev/null
@@ -1,175 +0,0 @@
-# Bytedance Speech Plugin
-
-火山引擎语音识别和语音合成插件,为Flutter应用提供高质量的语音识别(ASR)和语音合成(TTS)能力。
-
-## 功能特性
-
-- **语音识别(ASR)**
- - 支持实时语音识别
- - 支持流式输入和流式识别两种模式
- - 支持中文普通话等多种语言
- - 支持标点符号自动添加
-
-- **语音合成(TTS)**
- - 支持高质量语音合成
- - 支持多种音色选择
- - 支持多种音频格式输出
- - 支持使用大模型合成自然的语音
-
-## 安装
-
-将以下内容添加到您的`pubspec.yaml`文件中:
-
-```yaml
-dependencies:
- bytedance_speech:
- path: ../local_plugins/bytedance_speech
-```
-
-## 使用方法
-
-### 初始化插件
-
-```dart
-import 'package:bytedance_speech/bytedance_speech.dart';
-
-// 获取插件实例
-final bytedanceSpeech = BytedanceSpeech();
-
-// 初始化ASR
-await bytedanceSpeech.initAsr(
- AsrConfig(
- appId: 'YOUR_APP_ID',
- accessToken: 'YOUR_ACCESS_TOKEN',
- streamMode: true, // true: 双向流式模式, false: 流式输入模式
- enablePunctuation: true,
- sampleRate: 16000,
- bits: 16,
- channels: 1,
- ),
-);
-
-// 初始化TTS
-await bytedanceSpeech.initTts(
- TtsConfig(
- appId: 'YOUR_APP_ID',
- accessToken: 'YOUR_ACCESS_TOKEN',
- speaker: 'zh_female_shuangkuaisisi_moon_bigtts', // 选择音色
- format: 'mp3',
- sampleRate: 24000,
- enableTimestamp: true,
- ),
-);
-```
-
-### 语音识别(ASR)
-
-```dart
-// 监听ASR事件
-bytedanceSpeech.onAsrEvent.listen((event) {
- switch (event.type) {
- case AsrEventType.recognizing:
- // 处理中间识别结果
- print('识别中:${event.text}');
- break;
-
- case AsrEventType.result:
- // 处理最终识别结果
- print('识别结果:${event.text}');
- break;
-
- case AsrEventType.error:
- // 处理错误
- print('识别错误:${event.errorCode} - ${event.errorMessage}');
- break;
-
- case AsrEventType.sessionStarted:
- print('开始识别会话');
- break;
-
- case AsrEventType.sessionStopped:
- print('识别会话结束');
- break;
-
- default:
- break;
- }
-});
-
-// 开始识别
-await bytedanceSpeech.startAsr();
-
-// 发送音频数据
-await bytedanceSpeech.feedAudioData(audioBytes);
-
-// 停止识别
-await bytedanceSpeech.stopAsr();
-
-// 释放资源
-await bytedanceSpeech.disposeAsr();
-```
-
-### 语音合成(TTS)
-
-```dart
-// 监听TTS事件
-bytedanceSpeech.onTtsEvent.listen((event) {
- switch (event.type) {
- case TtsEventType.audio:
- // 处理音频数据
- if (event.audioData != null) {
- // 播放或保存音频数据
- playAudio(event.audioData!);
- }
- break;
-
- case TtsEventType.completed:
- // 合成完成
- print('语音合成完成');
- break;
-
- case TtsEventType.error:
- // 处理错误
- print('合成错误:${event.errorCode} - ${event.errorMessage}');
- break;
-
- case TtsEventType.started:
- print('开始合成');
- break;
-
- default:
- break;
- }
-});
-
-// 合成文本
-await bytedanceSpeech.synthesize('您好,欢迎使用火山引擎语音合成服务。');
-
-// 停止合成
-await bytedanceSpeech.stopTts();
-
-// 释放资源
-await bytedanceSpeech.disposeTts();
-```
-
-## 音色列表
-
-以下是部分可用的音色列表:
-
-| 音色ID | 描述 |
-|--------|------|
-| zh_female_shuangkuaisisi_moon_bigtts | 中文女声-爽快思思 |
-| zh_male_zhiqiangqikai_moon_bigtts | 中文男声-志强起凯 |
-| zh_female_xiaoya_moon_bigtts | 中文女声-小雅 |
-| zh_male_xiaoming_moon_bigtts | 中文男声-小明 |
-
-完整的音色列表,请参考火山引擎官方文档:[火山引擎音色列表](https://www.volcengine.com/docs/6561/123172)
-
-## 许可证
-
-本插件基于MIT许可证开源。
-
-## 相关链接
-
-- [火山引擎官网](https://www.volcengine.com/)
-- [火山引擎语音服务文档](https://www.volcengine.com/docs/6561/65067)
\ No newline at end of file
diff --git a/apps/client/local_plugins/bytedance_speech/android/build.gradle.kts b/apps/client/local_plugins/bytedance_speech/android/build.gradle.kts
deleted file mode 100644
index 402e0b7b..00000000
--- a/apps/client/local_plugins/bytedance_speech/android/build.gradle.kts
+++ /dev/null
@@ -1,56 +0,0 @@
-group = "com.yunqiinnovation.bytedance_speech"
-version = "1.0.0"
-
-plugins {
- id("com.android.library")
- kotlin("android")
-}
-
-buildscript {
- repositories {
- google()
- mavenCentral()
- }
-
- dependencies {
- classpath("com.android.tools.build:gradle:8.1.0")
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
- }
-}
-
-repositories {
- google()
- mavenCentral()
-}
-
-android {
- namespace = "com.deep_voice.bytedance_speech"
- compileSdk = 35
-
- defaultConfig {
- minSdk = 26
- targetSdk = 35
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
-
- kotlinOptions {
- jvmTarget = "1.8"
- languageVersion = "1.7"
- apiVersion = "1.7"
- }
-}
-
-dependencies {
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
- implementation("com.squareup.okhttp3:okhttp:4.11.0")
- implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
- implementation("com.google.code.gson:gson:2.10.1")
-
-
- implementation(project(":speech"))
-}
\ No newline at end of file
diff --git a/apps/client/local_plugins/bytedance_speech/android/settings.gradle.kts b/apps/client/local_plugins/bytedance_speech/android/settings.gradle.kts
deleted file mode 100644
index 0ee463e3..00000000
--- a/apps/client/local_plugins/bytedance_speech/android/settings.gradle.kts
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = "bytedance_speech"
\ No newline at end of file
diff --git a/apps/client/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml b/apps/client/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml
deleted file mode 100644
index 3fcca60b..00000000
--- a/apps/client/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt b/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt
deleted file mode 100644
index 9e9da5ee..00000000
--- a/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt
+++ /dev/null
@@ -1,730 +0,0 @@
-package com.deep_voice.bytedance_speech
-
-import android.content.Context
-import android.util.Log
-import com.google.gson.Gson
-import com.google.gson.JsonObject
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import okhttp3.WebSocket
-import okhttp3.WebSocketListener
-import okhttp3.logging.HttpLoggingInterceptor
-import okhttp3.logging.HttpLoggingInterceptor.Level
-import okio.ByteString
-import org.json.JSONObject
-import java.io.ByteArrayOutputStream
-import java.util.UUID
-import java.util.concurrent.TimeUnit
-import java.util.zip.GZIPInputStream
-import java.util.zip.GZIPOutputStream
-
-/**
- * 火山引擎大模型语音识别实现
- *
- * 基于WebSocket协议实现实时语音识别
- */
-class BytedanceASR(
- private val context: Context,
- private val resultCallback: (String, Boolean) -> Unit,
- private val errorCallback: (String, String) -> Unit,
- private val statusCallback: (String) -> Unit
-) {
- companion object {
- private const val TAG = "BytedanceASR"
- // 协议常量
- private const val PROTOCOL_VERSION = 0x01
- private const val DEFAULT_HEADER_SIZE = 0x01
-
- // 消息类型
- private const val FULL_CLIENT_REQUEST = 0x01
- private const val AUDIO_ONLY_REQUEST = 0x02
- private const val FULL_SERVER_RESPONSE = 0x09
- private const val SERVER_ACK = 0x0B
- private const val SERVER_ERROR_RESPONSE = 0x0F
-
- // 消息类型特定标志
- private const val NO_SEQUENCE = 0x00
- private const val POS_SEQUENCE = 0x01
- private const val NEG_SEQUENCE = 0x02
- private const val NEG_WITH_SEQUENCE = 0x03
-
- // 序列化方法
- private const val NO_SERIALIZATION = 0x00
- private const val JSON = 0x01
-
- // 压缩方法
- private const val NO_COMPRESSION = 0x00
- private const val GZIP = 0x01
-
- // 状态常量
- private const val STATUS_INITIALIZING = "initializing"
- private const val STATUS_READY = "ready"
- private const val STATUS_RECOGNIZING = "recognizing"
- private const val STATUS_STOPPED = "stopped"
- private const val STATUS_ERROR = "error"
- }
-
- // WebSocket连接
- private var webSocket: WebSocket? = null
- private val client: OkHttpClient
- private var seq = 1
-
- // 配置参数
- private var appId: String = ""
- private var accessToken: String = ""
- private var streamMode: Boolean = true
- private var enablePunctuation: Boolean = true
- private var sampleRate: Int = 16000
- private var bits: Int = 16
- private var channels: Int = 1
- private var supportedLanguages: List = listOf("zh-CN")
-
- // 状态控制
- private var isRecognizing = false
- private var isLastPacket = false
- private var isContinuousMode = false
- private var isInitialized = false
- private var currentStatus = STATUS_STOPPED
-
- // 单次识别结果收集
- private var onceResultBuilder = StringBuilder()
- private var singleRecognitionCallback: ((String) -> Unit)? = null
-
- init {
- // 配置OkHttp客户端
- val loggingInterceptor = HttpLoggingInterceptor().apply {
- level = Level.HEADERS
- }
-
- client = OkHttpClient.Builder()
- .pingInterval(30, TimeUnit.SECONDS)
- .addInterceptor(loggingInterceptor)
- .readTimeout(60, TimeUnit.SECONDS)
- .writeTimeout(60, TimeUnit.SECONDS)
- .build()
- }
-
- /**
- * 初始化语音识别服务
- */
- suspend fun initialize(
- subscriptionKey: String,
- region: String,
- supportedLanguages: List? = null
- ): Boolean = withContext(Dispatchers.IO) {
- try {
- updateStatus(STATUS_INITIALIZING)
-
- // 设置必要参数
- this@BytedanceASR.appId = region // 火山引擎中region对应应用ID
- this@BytedanceASR.accessToken = subscriptionKey // 订阅密钥作为访问令牌
-
- if (!supportedLanguages.isNullOrEmpty()) {
- this@BytedanceASR.supportedLanguages = supportedLanguages
- }
-
- // 释放旧资源
- release()
-
- isInitialized = true
- updateStatus(STATUS_READY)
- true
- } catch (e: Exception) {
- errorCallback("INIT_ERROR", e.message ?: "初始化失败")
- updateStatus(STATUS_ERROR)
- false
- }
- }
-
- /**
- * 单次识别(非流式),完成后自动断开连接
- */
- suspend fun recognizeOnce(audioData: ByteArray, callback: (String) -> Unit): Boolean = withContext(Dispatchers.IO) {
- try {
- if (!isInitialized) {
- errorCallback("NOT_INITIALIZED", "请先调用initialize方法")
- return@withContext false
- }
-
- if (isRecognizing) {
- errorCallback("BUSY", "语音识别正在进行中")
- return@withContext false
- }
-
- // 重置状态
- isContinuousMode = false
- isLastPacket = false
- onceResultBuilder.clear()
- singleRecognitionCallback = callback
- streamMode = false // 使用非流式模式
-
- // 开始连接
- start()
-
- // 等待连接建立
- var attempts = 0
- while (!isRecognizing && attempts < 10) {
- delay(300)
- attempts++
- }
-
- if (!isRecognizing) {
- errorCallback("CONNECTION_ERROR", "无法建立连接")
- return@withContext false
- }
-
- // 发送音频数据
- feedAudioData(audioData)
-
- // 发送结束标志
- stop()
-
- true
- } catch (e: Exception) {
- errorCallback("RECOGNIZE_ERROR", e.message ?: "识别失败")
- updateStatus(STATUS_ERROR)
- false
- }
- }
-
- /**
- * 启动连续识别
- */
- suspend fun startContinuousRecognition(): Boolean = withContext(Dispatchers.IO) {
- try {
- if (!isInitialized) {
- errorCallback("NOT_INITIALIZED", "请先调用initialize方法")
- return@withContext false
- }
-
- if (isRecognizing) {
- errorCallback("BUSY", "语音识别正在进行中")
- return@withContext false
- }
-
- // 设置为连续模式
- isContinuousMode = true
- isLastPacket = false
- streamMode = true // 使用流式模式
-
- // 开始连接
- start()
-
- true
- } catch (e: Exception) {
- errorCallback("START_ERROR", e.message ?: "启动连续识别失败")
- updateStatus(STATUS_ERROR)
- false
- }
- }
-
- /**
- * 停止连续识别
- */
- suspend fun stopContinuousRecognition(): Boolean = withContext(Dispatchers.IO) {
- try {
- if (!isRecognizing || !isContinuousMode) {
- return@withContext true // 已经停止或非连续模式
- }
-
- // 发送结束标志
- stop()
-
- true
- } catch (e: Exception) {
- errorCallback("STOP_ERROR", e.message ?: "停止连续识别失败")
- false
- }
- }
-
- /**
- * 推送音频数据
- */
- suspend fun pushAudioData(audioData: ByteArray): Boolean = withContext(Dispatchers.IO) {
- try {
- if (!isRecognizing) {
- errorCallback("NOT_RUNNING", "语音识别未启动")
- return@withContext false
- }
-
- feedAudioData(audioData)
- true
- } catch (e: Exception) {
- errorCallback("AUDIO_PUSH_ERROR", e.message ?: "推送音频数据失败")
- false
- }
- }
-
- /**
- * 用于暂停执行的辅助函数
- */
- private suspend fun delay(ms: Long) = withContext(Dispatchers.IO) {
- try {
- Thread.sleep(ms)
- } catch (e: InterruptedException) {
- // 忽略中断异常
- }
- }
-
- /**
- * 更新状态并通知Dart层
- */
- private fun updateStatus(status: String) {
- currentStatus = status
- statusCallback(status)
- }
-
- /**
- * 开始语音识别
- */
- suspend fun start() = withContext(Dispatchers.IO) {
- if (isRecognizing) return@withContext
-
- isRecognizing = true
- isLastPacket = false
- seq = 1
-
- val url = if (streamMode) {
- "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel"
- } else {
- "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_nostream"
- }
-
- val request = Request.Builder()
- .url(url)
- .header("X-Api-App-Key", appId)
- .header("X-Api-Access-Key", accessToken)
- .header("X-Api-Resource-Id", "volc.bigasr.sauc.duration")
- .header("X-Api-Connect-Id", UUID.randomUUID().toString())
- .build()
-
- updateStatus(STATUS_RECOGNIZING)
-
- webSocket = client.newWebSocket(request, object : WebSocketListener() {
- override fun onOpen(webSocket: WebSocket, response: Response) {
- val logId = response.header("X-Tt-Logid")
- Log.d(TAG, "ASR WebSocket连接成功, X-Tt-Logid: $logId")
-
- // 发送初始配置请求
- sendInitialRequest(webSocket)
- }
-
- override fun onMessage(webSocket: WebSocket, text: String) {
- Log.d(TAG, "收到文本消息: $text")
- try {
- val json = JSONObject(text)
- // 解析结果
- resultCallback(json.optString("text", ""), false)
- } catch (e: Exception) {
- Log.e(TAG, "解析消息失败", e)
- }
- }
-
- override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
- // Log.d(TAG, "收到二进制消息")
- // 在实际实现中解析二进制消息
- }
-
- override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
- Log.d(TAG, "连接关闭中: $code, $reason")
- isRecognizing = false
- }
-
- override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
- Log.d(TAG, "连接已关闭: $code, $reason")
- isRecognizing = false
- }
-
- override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
- Log.e(TAG, "连接失败", t)
- errorCallback("CONNECTION_ERROR", t.message ?: "连接失败")
- isRecognizing = false
- }
- })
- }
-
- /**
- * 向服务器发送音频数据
- */
- suspend fun feedAudioData(audioData: ByteArray) {
- if (!isRecognizing || webSocket == null) {
- Log.w(TAG, "未开始识别或WebSocket未连接")
- return
- }
-
- try {
- // 在实际实现中需要按照火山引擎的协议格式封装音频数据
- val byteString = ByteString.of(*audioData)
- webSocket?.send(byteString)
- } catch (e: Exception) {
- Log.e(TAG, "发送音频数据失败", e)
- }
- }
-
- /**
- * 停止语音识别
- */
- suspend fun stop() {
- if (!isRecognizing) {
- return
- }
-
- try {
- // 发送最后一帧音频数据,标记结束
- // 在实际实现中需要按照协议发送结束标记
-
- isRecognizing = false
- webSocket?.close(1000, "正常关闭")
- webSocket = null
- } catch (e: Exception) {
- Log.e(TAG, "停止识别失败", e)
- }
- }
-
- /**
- * 释放资源
- */
- fun release() {
- webSocket?.cancel()
- webSocket = null
- isRecognizing = false
- isLastPacket = false
- updateStatus(STATUS_STOPPED)
- }
-
- /**
- * 发送初始配置请求
- */
- private fun sendInitialRequest(webSocket: WebSocket) {
- try {
- // 构建请求参数
- val user = JsonObject().apply {
- addProperty("uid", "test")
- }
-
- val audio = JsonObject().apply {
- addProperty("format", "pcm")
- addProperty("sample_rate", sampleRate)
- addProperty("bits", bits)
- addProperty("channel", channels)
- addProperty("codec", "raw")
- }
-
- val request = JsonObject().apply {
- addProperty("model_name", "bigmodel")
- addProperty("enable_punc", enablePunctuation)
- }
-
- val payload = JsonObject().apply {
- add("user", user)
- add("audio", audio)
- add("request", request)
- }
-
- val payloadStr = payload.toString()
- Log.d(TAG, "ASR初始请求: $payloadStr")
-
- // 压缩负载
- val compressedPayload = gzipCompress(payloadStr.toByteArray())
-
- // 构建消息头
- val header = getHeader(
- FULL_CLIENT_REQUEST,
- POS_SEQUENCE,
- JSON,
- GZIP,
- 0
- )
-
- // 构建消息序列号
- val seqBytes = intToBytes(seq)
-
- // 构建负载大小
- val payloadSizeBytes = intToBytes(compressedPayload.size)
-
- // 组装完整请求数据
- val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size)
- var offset = 0
-
- System.arraycopy(header, 0, requestBytes, offset, header.size)
- offset += header.size
-
- System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size)
- offset += seqBytes.size
-
- System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size)
- offset += payloadSizeBytes.size
-
- System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size)
-
- // 发送请求
- val byteString = ByteString.of(*requestBytes)
- webSocket.send(byteString)
- } catch (e: Exception) {
- Log.e(TAG, "发送初始请求失败", e)
- }
- }
-
- /**
- * 发送音频数据请求
- */
- private fun sendAudioRequest(webSocket: WebSocket, audioData: ByteArray, isLast: Boolean) {
- try {
- seq++
-
- // 如果是最后一个包,使用负序列号
- val actualSeq = if (isLast) -seq else seq
-
- // 设置消息标志
- val flag = if (isLast) NEG_WITH_SEQUENCE else POS_SEQUENCE
-
- // 构建消息头
- val header = getHeader(
- AUDIO_ONLY_REQUEST,
- flag,
- NO_SERIALIZATION,
- GZIP,
- 0
- )
-
- // 构建消息序列号
- val seqBytes = intToBytes(actualSeq)
-
- // 压缩音频数据
- val compressedPayload = if (audioData.isNotEmpty()) {
- gzipCompress(audioData)
- } else {
- ByteArray(0)
- }
-
- // 构建负载大小
- val payloadSizeBytes = intToBytes(compressedPayload.size)
-
- // 组装完整请求数据
- val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size)
- var offset = 0
-
- System.arraycopy(header, 0, requestBytes, offset, header.size)
- offset += header.size
-
- System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size)
- offset += seqBytes.size
-
- System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size)
- offset += payloadSizeBytes.size
-
- if (compressedPayload.isNotEmpty()) {
- System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size)
- }
-
- // 发送请求
- val byteString = ByteString.of(*requestBytes)
- webSocket.send(byteString)
- } catch (e: Exception) {
- Log.e(TAG, "发送音频请求失败", e)
- }
- }
-
- /**
- * 解析响应数据
- */
- private fun parseResponse(data: ByteArray): AsrResponse {
- val response = AsrResponse()
-
- // 解析头部 (4字节)
- response.protocolVersion = (data[0].toInt() shr 4) and 0x0F
- response.headerSize = data[0].toInt() and 0x0F
- response.messageType = (data[1].toInt() shr 4) and 0x0F
- response.messageTypeSpecificFlags = data[1].toInt() and 0x0F
- response.serialization = (data[2].toInt() shr 4) and 0x0F
- response.compression = data[2].toInt() and 0x0F
- response.reserved = data[3].toInt()
-
- // 解析序列号 (4字节)
- val seqBytes = ByteArray(4)
- System.arraycopy(data, 4, seqBytes, 0, 4)
- response.sequence = bytesToInt(seqBytes)
-
- // 解析负载大小 (4字节)
- val sizeBytes = ByteArray(4)
- System.arraycopy(data, 8, sizeBytes, 0, 4)
- val payloadSize = bytesToInt(sizeBytes)
-
- // 解析负载数据
- if (payloadSize > 0 && data.size > 12) {
- val payload = ByteArray(payloadSize)
- System.arraycopy(data, 12, payload, 0, payloadSize)
- response.payload = payload
- } else {
- response.payload = ByteArray(0)
- }
-
- return response
- }
-
- /**
- * 从JSON响应中提取文本
- */
- private fun extractTextFromJson(jsonStr: String): String {
- try {
- val gson = Gson()
- val jsonObject = gson.fromJson(jsonStr, JsonObject::class.java)
-
- // 提取识别结果文本
- if (jsonObject.has("result") && !jsonObject.get("result").isJsonNull) {
- val result = jsonObject.getAsJsonObject("result")
-
- if (result.has("text") && !result.get("text").isJsonNull) {
- return result.get("text").asString
- }
-
- // 尝试从segments中提取文本
- if (result.has("segments") && result.get("segments").isJsonArray) {
- val segments = result.getAsJsonArray("segments")
- val builder = StringBuilder()
-
- for (i in 0 until segments.size()) {
- val segment = segments.get(i).asJsonObject
- if (segment.has("text") && !segment.get("text").isJsonNull) {
- builder.append(segment.get("text").asString)
- }
- }
-
- return builder.toString()
- }
- }
-
- return ""
- } catch (e: Exception) {
- Log.e(TAG, "解析JSON失败", e)
- return ""
- }
- }
-
- /**
- * 获取消息头
- */
- private fun getHeader(
- messageType: Int,
- messageTypeSpecificFlags: Int,
- serialMethod: Int,
- compressionType: Int,
- reservedData: Int
- ): ByteArray {
- val header = ByteArray(4)
- header[0] = ((PROTOCOL_VERSION shl 4) or DEFAULT_HEADER_SIZE).toByte()
- header[1] = ((messageType shl 4) or messageTypeSpecificFlags).toByte()
- header[2] = ((serialMethod shl 4) or compressionType).toByte()
- header[3] = reservedData.toByte()
- return header
- }
-
- /**
- * 整数转字节数组 (大端序)
- */
- private fun intToBytes(value: Int): ByteArray {
- return byteArrayOf(
- ((value shr 24) and 0xFF).toByte(),
- ((value shr 16) and 0xFF).toByte(),
- ((value shr 8) and 0xFF).toByte(),
- (value and 0xFF).toByte()
- )
- }
-
- /**
- * 字节数组转整数 (大端序)
- */
- private fun bytesToInt(bytes: ByteArray): Int {
- if (bytes.size != 4) {
- throw IllegalArgumentException("字节数组长度必须为4")
- }
-
- return ((bytes[0].toInt() and 0xFF) shl 24) or
- ((bytes[1].toInt() and 0xFF) shl 16) or
- ((bytes[2].toInt() and 0xFF) shl 8) or
- (bytes[3].toInt() and 0xFF)
- }
-
- /**
- * GZIP压缩
- */
- private fun gzipCompress(data: ByteArray): ByteArray {
- if (data.isEmpty()) return ByteArray(0)
-
- val output = ByteArrayOutputStream()
- GZIPOutputStream(output).use { gzip ->
- gzip.write(data)
- }
-
- return output.toByteArray()
- }
-
- /**
- * GZIP解压缩
- */
- private fun gzipDecompress(data: ByteArray): ByteArray {
- if (data.isEmpty()) return ByteArray(0)
-
- val output = ByteArrayOutputStream()
- GZIPInputStream(data.inputStream()).use { gzip ->
- val buffer = ByteArray(1024)
- var len: Int
-
- while (gzip.read(buffer).also { len = it } > 0) {
- output.write(buffer, 0, len)
- }
- }
-
- return output.toByteArray()
- }
-
- /**
- * ASR响应数据结构
- */
- private data class AsrResponse(
- var protocolVersion: Int = 0,
- var headerSize: Int = 0,
- var messageType: Int = 0,
- var messageTypeSpecificFlags: Int = 0,
- var serialization: Int = 0,
- var compression: Int = 0,
- var reserved: Int = 0,
- var sequence: Int = 0,
- var payload: ByteArray = ByteArray(0)
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as AsrResponse
-
- if (protocolVersion != other.protocolVersion) return false
- if (headerSize != other.headerSize) return false
- if (messageType != other.messageType) return false
- if (messageTypeSpecificFlags != other.messageTypeSpecificFlags) return false
- if (serialization != other.serialization) return false
- if (compression != other.compression) return false
- if (reserved != other.reserved) return false
- if (sequence != other.sequence) return false
- if (!payload.contentEquals(other.payload)) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = protocolVersion
- result = 31 * result + headerSize
- result = 31 * result + messageType
- result = 31 * result + messageTypeSpecificFlags
- result = 31 * result + serialization
- result = 31 * result + compression
- result = 31 * result + reserved
- result = 31 * result + sequence
- result = 31 * result + payload.contentHashCode()
- return result
- }
- }
-}
\ No newline at end of file
diff --git a/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt b/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt
deleted file mode 100644
index f26eae6b..00000000
--- a/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt
+++ /dev/null
@@ -1,424 +0,0 @@
-package com.deep_voice.bytedance_speech
-
-import android.content.Context
-import android.media.AudioAttributes
-import android.media.AudioDeviceCallback
-import android.media.AudioDeviceInfo
-import android.media.AudioFormat
-import android.media.AudioManager
-import android.media.AudioTrack
-import android.os.Build
-import android.os.Handler
-import android.os.Looper
-import android.util.Log
-import com.deep_voice.speech.tts.AudioDataListener
-import com.deep_voice.speech.tts.AudioOutputDevice
-
-/**
- * 语音通信音频播放器 - 16kHz采样率
- * 设计要点:
- * • 使用 USAGE_VOICE_COMMUNICATION + MODE_IN_COMMUNICATION 通信通路
- * • 利用 AudioDeviceCallback 监听设备热插拔
- * • API 31+ 使用 setPreferredDevice() 精准路由
- * • 有耳机自动从耳机播放,无耳机自动切到扬声器
- * • 内置回声消除和噪音抑制功能
- */
-class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, AudioDeviceCallback() {
- companion object {
- private const val TAG = "BytedanceAudioPlayer"
- private const val SAMPLE_RATE = 16000 // 固定16kHz采样率
- private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO
- private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
- }
-
- private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
- private var audioTrack: AudioTrack? = null
- private var isFirstData = true
- private var totalBytesWritten = 0
- private var sessionActive = true
- private var audioFocusRequest: Any? = null // 兼容不同API版本
-
- // 音频焦点变化监听器
- private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
- when (focusChange) {
- AudioManager.AUDIOFOCUS_LOSS -> {
- // 永久失去焦点,停止播放
- Log.d(TAG, "音频焦点丢失")
- stop()
- }
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
- // 暂时失去焦点,暂停播放
- Log.d(TAG, "音频焦点暂时丢失")
- audioTrack?.pause()
- }
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
- // 暂时失去焦点但可降低音量,这里选择暂停
- Log.d(TAG, "音频焦点暂时丢失(可降低音量)")
- audioTrack?.pause()
- }
- AudioManager.AUDIOFOCUS_GAIN -> {
- // 重新获得焦点,恢复播放
- Log.d(TAG, "音频焦点恢复")
- if (sessionActive) {
- audioTrack?.play()
- }
- }
- }
- }
-
- // 回调
- private var onPlayStarted: (() -> Unit)? = null
- private var onPlayCompleted: (() -> Unit)? = null
-
- // 耳机设备类型(优先级顺序)
- private val headsetTypes = intArrayOf(
- AudioDeviceInfo.TYPE_WIRED_HEADSET,
- AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
- AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
- // TYPE_BLUETOOTH_LE 在较新的 API 中才有,这里先不包含
- )
-
- // 播放位置监听器
- private val playbackListener = object : AudioTrack.OnPlaybackPositionUpdateListener {
- override fun onMarkerReached(track: AudioTrack) {
- onPlayCompleted?.invoke()
- }
-
- override fun onPeriodicNotification(track: AudioTrack) {
- // 不使用周期性通知
- }
- }
-
- init {
- // 注册设备热插拔监听
- audioManager.registerAudioDeviceCallback(this, Handler(Looper.getMainLooper()))
- // 请求音频焦点
- requestAudioFocus()
- buildTrack(selectOutputDevice()) // 首次创建
- }
-
- /**
- * 监听输出设备变化
- */
- override fun onAudioDevicesAdded(addedDevices: Array) {
- Log.d(TAG, "音频设备连接: ${addedDevices.joinToString { "${it.productName}(${it.type})" }}")
- rebuildIfNeeded()
- }
-
- override fun onAudioDevicesRemoved(removedDevices: Array) {
- Log.d(TAG, "音频设备断开: ${removedDevices.joinToString { "${it.productName}(${it.type})" }}")
- rebuildIfNeeded()
- }
-
- /**
- * 检查设备变化,必要时重建AudioTrack
- */
- private fun rebuildIfNeeded() {
- val newDevice = selectOutputDevice()
- val currentDevice = audioTrack?.preferredDevice
-
- // 设备发生变化时重建
- if (newDevice?.id != currentDevice?.id) {
- Log.d(TAG, "设备切换: ${currentDevice?.productName} -> ${newDevice?.productName}")
-
- // 保存当前状态
- val wasPlaying = audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING
- val savedFirstData = isFirstData
- val savedTotalBytes = totalBytesWritten
- val savedSessionActive = sessionActive
-
- // 重建AudioTrack
- buildTrack(newDevice)
-
- // 恢复状态
- if (wasPlaying) {
- isFirstData = savedFirstData
- totalBytesWritten = savedTotalBytes
- sessionActive = savedSessionActive
- }
- }
- }
-
- /**
- * 选择当前优先输出设备(耳机优先)
- */
- private fun selectOutputDevice(): AudioDeviceInfo? {
- return audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
- .firstOrNull { it.type in headsetTypes }
- }
-
- /**
- * 构建16kHz单声道AudioTrack
- */
- private fun buildTrack(device: AudioDeviceInfo?) {
- audioTrack?.release()
-
- val minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT)
-
- audioTrack = AudioTrack.Builder()
- .setAudioAttributes(
- AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) // 语音通信通路
- .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
- .build()
- )
- .setAudioFormat(
- AudioFormat.Builder()
- .setSampleRate(SAMPLE_RATE)
- .setEncoding(AUDIO_FORMAT)
- .setChannelMask(CHANNEL_CONFIG)
- .build()
- )
- .setTransferMode(AudioTrack.MODE_STREAM)
- .setBufferSizeInBytes(minBufferSize * 2) // 小缓冲区保证低延迟
- .apply {
- // 低延迟模式
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
- }
- }
- .build().also { track ->
- // API 31+ 精准路由到指定设备
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && device != null) {
- track.preferredDevice = device
- Log.d(TAG, "设置首选设备: ${device.productName} (${device.type})")
- }
-
- // 设置播放监听器
- track.setPlaybackPositionUpdateListener(playbackListener)
-
- // 开始播放
- track.play()
- }
-
- // 配置音频管理器 - 使用通信模式以启用回声消除和噪音抑制
- audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
- // 切换扬声器开关仅在无耳机时打开
- audioManager.isSpeakerphoneOn = (device == null)
-
- val deviceName = device?.productName ?: "扬声器"
- Log.d(TAG, "AudioTrack已创建 - 输出设备: $deviceName, 采样率: ${SAMPLE_RATE}Hz")
- }
-
- /**
- * 请求音频焦点
- */
- private fun requestAudioFocus() {
- try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- // Android O及以上使用新的API
- val focusRequest = android.media.AudioFocusRequest.Builder(
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
- )
- .setAudioAttributes(
- AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
- .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
- .build()
- )
- .setAcceptsDelayedFocusGain(true)
- .setOnAudioFocusChangeListener(audioFocusChangeListener)
- .build()
-
- val result = audioManager.requestAudioFocus(focusRequest)
- if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- audioFocusRequest = focusRequest
- Log.d(TAG, "音频焦点请求成功")
- } else {
- Log.w(TAG, "音频焦点请求失败")
- }
- } else {
- // Android O以下使用旧的API
- @Suppress("DEPRECATION")
- val result = audioManager.requestAudioFocus(
- audioFocusChangeListener,
- AudioManager.STREAM_VOICE_CALL,
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
- )
- if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- Log.d(TAG, "音频焦点请求成功(旧API)")
- } else {
- Log.w(TAG, "音频焦点请求失败(旧API)")
- }
- }
- } catch (e: Exception) {
- Log.e(TAG, "请求音频焦点异常: ${e.message}")
- }
- }
-
- /**
- * 释放音频焦点
- */
- private fun releaseAudioFocus() {
- try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- audioFocusRequest?.let {
- audioManager.abandonAudioFocusRequest(it as android.media.AudioFocusRequest)
- }
- } else {
- @Suppress("DEPRECATION")
- audioManager.abandonAudioFocus(audioFocusChangeListener)
- }
- Log.d(TAG, "音频焦点已释放")
- } catch (e: Exception) {
- Log.e(TAG, "释放音频焦点异常: ${e.message}")
- }
- }
-
- /**
- * 开始新的播放会话
- */
- fun startSession() {
- isFirstData = true
- totalBytesWritten = 0
- sessionActive = true
-
- audioTrack?.let { track ->
- if (track.state == AudioTrack.STATE_INITIALIZED) {
- track.pause()
- track.flush()
- track.play()
- }
- } ?: buildTrack(selectOutputDevice())
- }
-
- /**
- * 标记数据流结束
- */
- fun endSession() {
- if (!sessionActive) return
-
- sessionActive = false
-
- audioTrack?.let { track ->
- if (totalBytesWritten > 0) {
- val totalFrames = totalBytesWritten / 2
- try {
- track.setNotificationMarkerPosition(totalFrames)
- } catch (e: Exception) {
- val durationMs = (totalFrames * 1000L) / SAMPLE_RATE
- Handler(Looper.getMainLooper()).postDelayed({
- onPlayCompleted?.invoke()
- }, durationMs + 100) // 更短的延迟
- }
- } else {
- onPlayCompleted?.invoke()
- }
- }
- }
-
- /**
- * 接收音频数据
- */
- override fun onAudioData(data: ByteArray) {
- if (data.isEmpty() || !sessionActive) return
- if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) return
-
- try {
- audioTrack?.let { track ->
- val bytesWritten = track.write(data, 0, data.size)
-
- if (bytesWritten > 0) {
- if (isFirstData) {
- isFirstData = false
- onPlayStarted?.invoke()
- }
-
- if (sessionActive) {
- totalBytesWritten += bytesWritten
- }
- }
- }
- } catch (e: Exception) {
- Log.w(TAG, "音频数据写入失败: ${e.message}")
- }
- }
-
- /**
- * 停止播放
- */
- fun stop() {
- sessionActive = false
-
- audioTrack?.let { track ->
- if (track.state == AudioTrack.STATE_INITIALIZED) {
- track.pause()
- track.flush()
- }
- }
-
- if (!isFirstData) {
- onPlayCompleted?.invoke()
- }
- }
-
- /**
- * 释放资源
- */
- fun release() {
- stop()
- audioTrack?.release()
- audioTrack = null
- releaseAudioFocus()
- audioManager.unregisterAudioDeviceCallback(this)
- // 恢复音频模式
- audioManager.mode = AudioManager.MODE_NORMAL
- }
-
- /**
- * 设置播放开始回调
- */
- fun setOnPlayStarted(listener: () -> Unit) {
- onPlayStarted = listener
- }
-
- /**
- * 设置播放完成回调
- */
- fun setOnPlayCompleted(listener: () -> Unit) {
- onPlayCompleted = listener
- }
-
- /**
- * 设置错误回调(兼容接口)
- */
- fun setOnError(listener: (String) -> Unit) {
- // 不实现,仅为兼容
- }
-
- /**
- * 检查是否正在播放
- */
- fun isPlaying(): Boolean {
- return audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING
- }
-
- /**
- * 设置音频输出设备(兼容接口)
- * 注意:此实现依赖自动设备切换,手动设置可能被热插拔覆盖
- */
- fun setAudioOutputDevice(device: AudioOutputDevice) {
- // 本实现专注于自动设备切换
- // 手动设置会在下次设备变化时被自动逻辑覆盖
- Log.d(TAG, "setAudioOutputDevice被调用,但本实现依赖自动设备切换")
- }
-
- /**
- * 获取当前路由设备信息(用于调试)
- */
- fun getCurrentDevice(): String {
- val routedDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- audioTrack?.routedDevice
- } else {
- null
- }
-
- val preferredDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- audioTrack?.preferredDevice
- } else {
- null
- }
-
- return "路由设备: ${routedDevice?.productName}, 首选设备: ${preferredDevice?.productName}"
- }
-}
\ No newline at end of file
diff --git a/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt b/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt
deleted file mode 100644
index a6003e05..00000000
--- a/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt
+++ /dev/null
@@ -1,485 +0,0 @@
-package com.deep_voice.bytedance_speech
-
-import android.content.Context
-import android.os.Handler
-import android.os.Looper
-import android.util.Log
-import androidx.annotation.NonNull
-import io.flutter.embedding.engine.plugins.FlutterPlugin
-import io.flutter.plugin.common.EventChannel
-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 kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import com.deep_voice.speech.tts.TtsEvent
-import com.deep_voice.speech.tts.TtsEventListener
-import com.deep_voice.speech.tts.TtsEventType
-import com.deep_voice.speech.tts.AudioDataListener
-import java.util.concurrent.ConcurrentLinkedQueue
-
-/**
- * 字节跳动语音服务Flutter插件
- * 提供TTS和ASR能力,通过独立的事件通道进行通信
- */
-class BytedanceSpeechPlugin : FlutterPlugin {
- companion object {
- private const val TAG = "BytedanceSpeechPlugin"
-
- // 通道名称
- private const val TTS_METHOD_CHANNEL = "com.deep_voice.bytedance_tts"
- private const val ASR_METHOD_CHANNEL = "com.deep_voice.bytedance_asr"
-
- // 事件通道
- private const val TTS_EVENT_CHANNEL = "com.deep_voice.bytedance_tts/events"
- private const val TTS_AUDIO_CHANNEL = "com.deep_voice.bytedance_tts/audio"
- private const val ASR_EVENT_CHANNEL = "com.deep_voice.bytedance_asr/events"
- }
-
- // Flutter插件相关
- private lateinit var ttsChannel: MethodChannel
- private lateinit var asrChannel: MethodChannel
- private lateinit var context: Context
-
- // TTS事件通道
- private var ttsEventSink: EventChannel.EventSink? = null
- private var ttsAudioSink: EventChannel.EventSink? = null
-
- // ASR事件通道
- private var asrEventSink: EventChannel.EventSink? = null
-
- // 语音服务实例
- private var bytedanceTTS: BytedanceTTS? = null
- private var bytedanceASR: BytedanceASR? = null
-
- // 协程作用域
- private val job = SupervisorJob()
- private val scope = CoroutineScope(Dispatchers.Main + job)
-
- // 主线程Handler
- private val mainHandler = Handler(Looper.getMainLooper())
-
- override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
- context = flutterPluginBinding.applicationContext
-
- // 初始化TTS方法通道
- ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, TTS_METHOD_CHANNEL)
- ttsChannel.setMethodCallHandler(TtsMethodHandler())
-
- // 初始化ASR方法通道
- asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, ASR_METHOD_CHANNEL)
- asrChannel.setMethodCallHandler(AsrMethodHandler())
-
- // 设置TTS事件通道
- setupTtsEventChannels(flutterPluginBinding)
-
- // 设置ASR事件通道
- setupAsrEventChannel(flutterPluginBinding)
-
- Log.d(TAG, "字节跳动语音插件已附加到Flutter引擎")
- }
-
- /**
- * 设置TTS相关的事件通道
- */
- private fun setupTtsEventChannels(binding: FlutterPlugin.FlutterPluginBinding) {
- // TTS事件通道
- EventChannel(binding.binaryMessenger, TTS_EVENT_CHANNEL).setStreamHandler(
- object : EventChannel.StreamHandler {
- override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
- ttsEventSink = events
- Log.d(TAG, "TTS事件流已建立连接")
- }
-
- override fun onCancel(arguments: Any?) {
- ttsEventSink = null
- Log.d(TAG, "TTS事件流已取消")
- }
- }
- )
-
- // TTS音频数据通道
- EventChannel(binding.binaryMessenger, TTS_AUDIO_CHANNEL).setStreamHandler(
- object : EventChannel.StreamHandler {
- override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
- ttsAudioSink = events
- Log.d(TAG, "TTS音频流已建立连接")
- }
-
- override fun onCancel(arguments: Any?) {
- ttsAudioSink = null
- Log.d(TAG, "TTS音频流已取消")
- }
- }
- )
- }
-
- /**
- * 设置ASR相关的事件通道
- */
- private fun setupAsrEventChannel(binding: FlutterPlugin.FlutterPluginBinding) {
- // ASR事件通道
- EventChannel(binding.binaryMessenger, ASR_EVENT_CHANNEL).setStreamHandler(
- object : EventChannel.StreamHandler {
- override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
- asrEventSink = events
- Log.d(TAG, "ASR事件流已建立连接")
- }
-
- override fun onCancel(arguments: Any?) {
- asrEventSink = null
- Log.d(TAG, "ASR事件流已取消")
- }
- }
- )
- }
-
- /**
- * TTS方法处理器
- */
- inner class TtsMethodHandler : MethodCallHandler {
- override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
- when (call.method) {
- "initialize" -> {
- val ttsAppId = call.argument("ttsAppId") ?: ""
- val ttsAppToken = call.argument("ttsAppToken") ?: ""
- val language = call.argument("language") ?: "zh-CN"
-
- initializeTTS(ttsAppId, ttsAppToken, language, result)
- }
- "setVoice" -> {
- val voice = call.argument("voice") ?: ""
- setVoice(voice, result)
- }
- "speakOnce" -> {
- val text = call.argument("text") ?: ""
- speakOnce(text, result)
- }
- "speakStream" -> {
- val text = call.argument("text") ?: ""
- speakStream(text, result)
- }
- "flushStream" -> {
- flushStream(result)
- }
- "stopTTS" -> {
- stopTTS(result)
- }
- "disposeTTS" -> {
- disposeTTS(result)
- }
-
- else -> {
- result.notImplemented()
- }
- }
- }
- }
-
- /**
- * ASR方法处理器
- */
- inner class AsrMethodHandler : MethodCallHandler {
- override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
- when (call.method) {
- "initializeASR" -> {
- result.notImplemented()
- }
- "startListening" -> {
- result.notImplemented()
- }
- "stopListening" -> {
- result.notImplemented()
- }
- "cancelASR" -> {
- result.notImplemented()
- }
- else -> {
- result.notImplemented()
- }
- }
- }
- }
-
- override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
- ttsChannel.setMethodCallHandler(null)
- asrChannel.setMethodCallHandler(null)
-
- // 释放资源
- bytedanceTTS?.release()
- bytedanceTTS = null
-
- bytedanceASR?.release()
- bytedanceASR = null
-
- job.cancel()
-
- Log.d(TAG, "字节跳动语音插件已从Flutter引擎分离")
- }
-
- /**
- * 初始化TTS服务
- */
- private fun initializeTTS(ttsAppId: String, ttsAppToken: String, language: String, result: Result) {
- scope.launch {
- try {
- if (ttsAppId.isEmpty() || ttsAppToken.isEmpty()) {
- result.error(
- "INVALID_PARAMS",
- "AppID和AppToken不能为空",
- null
- )
- return@launch
- }
-
- // 确保只有一个TTS实例
- bytedanceTTS?.release()
-
- // 创建新的TTS实例
- bytedanceTTS = BytedanceTTS(context)
-
- // 添加事件监听器
- bytedanceTTS?.addListener(object : TtsEventListener {
- override fun onEvent(event: TtsEvent) {
- when (event.type) {
- TtsEventType.SYNTHESIS_STARTED -> {
- sendTTSEvent("started", null)
- }
- TtsEventType.SYNTHESIS_COMPLETED -> {
- sendTTSEvent("completed", null)
- }
- TtsEventType.SYNTHESIS_CANCELED -> {
- sendTTSEvent("canceled", null)
- }
- TtsEventType.PLAYBACK_STARTED -> {
- sendTTSEvent("playback_started", null)
- }
- TtsEventType.PLAYBACK_COMPLETED -> {
- sendTTSEvent("playback_completed", null)
- }
- TtsEventType.ERROR -> {
- val params = HashMap()
- params["code"] = event.params["errorCode"] ?: "UNKNOWN_ERROR"
- params["message"] = event.params["errorMessage"] ?: "未知错误"
- sendTTSEvent("error", params)
- }
- }
- }
- })
-
- // 添加音频数据监听器
- bytedanceTTS?.addAudioDataListener(object : AudioDataListener {
- override fun onAudioData(data: ByteArray) {
- sendTTSAudioData(data)
- }
- })
-
- // 初始化TTS
- val success = bytedanceTTS?.initialize(
- ttsAppId = ttsAppId,
- ttsAppToken = ttsAppToken,
- ttsResource = "", // 火山TTS不使用resource
- language = language
- ) ?: false
-
- if (success) {
- result.success(true)
- } else {
- result.error(
- "INIT_FAILED",
- "TTS初始化失败",
- null
- )
- }
- } catch (e: Exception) {
- Log.e(TAG, "TTS初始化异常: ${e.message}", e)
- result.error(
- "INIT_EXCEPTION",
- "TTS初始化异常: ${e.message}",
- null
- )
- }
- }
- }
-
- /**
- * 设置TTS音色
- */
- private fun setVoice(voice: String, result: Result) {
- if (bytedanceTTS == null) {
- result.error("NOT_INITIALIZED", "TTS未初始化", null)
- return
- }
-
- try {
- val success = bytedanceTTS?.setVoice(voice) ?: false
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "设置音色异常: ${e.message}", e)
- result.error(
- "VOICE_EXCEPTION",
- "设置音色异常: ${e.message}",
- null
- )
- }
- }
-
- /**
- * 合成并播放文本(单次)
- */
- private fun speakOnce(text: String, result: Result) {
- if (bytedanceTTS == null) {
- result.error("NOT_INITIALIZED", "TTS未初始化", null)
- return
- }
-
- try {
- val success = bytedanceTTS?.speakOnce(text) ?: false
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "合成文本异常: ${e.message}", e)
- result.error(
- "SPEAK_EXCEPTION",
- "合成文本异常: ${e.message}",
- null
- )
- }
- }
-
- /**
- * 流式合成文本
- */
- private fun speakStream(text: String, result: Result) {
- if (bytedanceTTS == null) {
- result.error("NOT_INITIALIZED", "TTS未初始化", null)
- return
- }
-
- try {
- val success = bytedanceTTS?.speakStream(text) ?: false
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "流式合成异常: ${e.message}", e)
- result.error(
- "STREAM_EXCEPTION",
- "流式合成异常: ${e.message}",
- null
- )
- }
- }
-
- /**
- * 刷新流式合成缓冲区
- */
- private fun flushStream(result: Result) {
- if (bytedanceTTS == null) {
- result.error("NOT_INITIALIZED", "TTS未初始化", null)
- return
- }
-
- try {
- val success = bytedanceTTS?.flushStream() ?: false
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "刷新流异常: ${e.message}", e)
- result.error(
- "FLUSH_EXCEPTION",
- "刷新流异常: ${e.message}",
- null
- )
- }
- }
-
- /**
- * 停止TTS
- */
- private fun stopTTS(result: Result) {
- if (bytedanceTTS == null) {
- result.success(true)
- return
- }
-
- try {
- val success = bytedanceTTS?.stop() ?: true
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "停止TTS异常: ${e.message}", e)
- result.error(
- "STOP_EXCEPTION",
- "停止TTS异常: ${e.message}",
- null
- )
- }
- }
-
- /**
- * 释放TTS资源
- */
- private fun disposeTTS(result: Result) {
- try {
- bytedanceTTS?.release()
- bytedanceTTS = null
- result.success(true)
- } catch (e: Exception) {
- Log.e(TAG, "释放TTS资源异常: ${e.message}", e)
- result.error(
- "DISPOSE_EXCEPTION",
- "释放TTS资源异常: ${e.message}",
- null
- )
- }
- }
-
-
- /**
- * 发送TTS事件到Flutter端
- */
- private fun sendTTSEvent(eventName: String, params: Map?) {
- if (ttsEventSink == null) return
-
- val event = HashMap()
- event["event"] = eventName
-
- if (params != null) {
- event["params"] = params
- }
-
- mainHandler.post {
- ttsEventSink?.success(event)
- }
- }
-
- /**
- * 发送TTS音频数据到Flutter端
- */
- private fun sendTTSAudioData(data: ByteArray) {
- if (ttsAudioSink == null) return
-
- mainHandler.post {
- ttsAudioSink?.success(data)
- }
- }
-
- /**
- * 发送ASR事件到Flutter端
- */
- private fun sendASREvent(eventType: String, data: Any, params: Map? = null) {
- if (asrEventSink == null) return
-
- val event = HashMap()
- event["type"] = eventType
- event["data"] = data
-
- if (params != null) {
- event["params"] = params
- }
-
- mainHandler.post {
- asrEventSink?.success(event)
- }
- }
-}
\ No newline at end of file
diff --git a/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt b/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt
deleted file mode 100644
index 6493340f..00000000
--- a/apps/client/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt
+++ /dev/null
@@ -1,1201 +0,0 @@
-package com.deep_voice.bytedance_speech
-
-import android.content.Context
-import android.util.Log
-import com.deep_voice.speech.tts.AudioDataListener
-import com.deep_voice.speech.tts.AudioOutputDevice
-import com.deep_voice.speech.tts.ITtsService
-import com.deep_voice.speech.tts.TtsEvent
-import com.deep_voice.speech.tts.TtsEventListener
-import com.deep_voice.speech.tts.TtsEventType
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import kotlinx.coroutines.CancellationException
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import okhttp3.WebSocket
-import okhttp3.WebSocketListener
-import okio.ByteString
-import org.json.JSONObject
-import java.util.UUID
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicBoolean
-
-/**
- * 火山引擎大模型语音合成实现
- *
- * 基于WebSocket协议实现双向流式TTS
- */
-class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
- companion object {
- private const val TAG = "BytedanceTTS"
-
- // 协议常量
- private const val PROTOCOL_VERSION = 0x01
- private const val DEFAULT_HEADER_SIZE = 0x01
-
- // 消息类型
- private const val FULL_CLIENT_REQUEST = 0x01
- private const val AUDIO_ONLY_RESPONSE = 0x0B
- private const val FULL_SERVER_RESPONSE = 0x09
- private const val ERROR_INFORMATION = 0x0F
-
- // 消息类型特定标志
- private const val MSG_TYPE_FLAG_NO_SEQ = 0x00
- private const val MSG_TYPE_FLAG_POSITIVE_SEQ = 0x01
- private const val MSG_TYPE_FLAG_LAST_NO_SEQ = 0x02
- private const val MSG_TYPE_FLAG_NEGATIVE_SEQ = 0x03
- private const val MSG_TYPE_FLAG_WITH_EVENT = 0x04
-
- // 序列化方法
- private const val NO_SERIALIZATION = 0x00
- private const val JSON = 0x01
-
- // 压缩方法
- private const val COMPRESSION_NO = 0x00
- private const val COMPRESSION_GZIP = 0x01
-
- // 事件类型
- private const val EVENT_NONE = 0
- private const val EVENT_START_CONNECTION = 1
- private const val EVENT_FINISH_CONNECTION = 2
- private const val EVENT_CONNECTION_STARTED = 50
- private const val EVENT_CONNECTION_FAILED = 51
- private const val EVENT_CONNECTION_FINISHED = 52
- private const val EVENT_START_SESSION = 100
- private const val EVENT_FINISH_SESSION = 102
- private const val EVENT_SESSION_STARTED = 150
- private const val EVENT_SESSION_FINISHED = 152
- private const val EVENT_SESSION_FAILED = 153
- private const val EVENT_TASK_REQUEST = 200
- private const val EVENT_TTS_SENTENCE_START = 350
- private const val EVENT_TTS_SENTENCE_END = 351
- private const val EVENT_TTS_RESPONSE = 352
-
- // 状态常量
- private const val STATUS_INITIALIZING = "initializing"
- private const val STATUS_READY = "ready"
- private const val STATUS_SPEAKING = "speaking"
- private const val STATUS_STOPPED = "stopped"
- private const val STATUS_ERROR = "error"
-
- // 连接控制常量
- private const val MAX_CONNECTION_ATTEMPTS = 3
- private const val CONNECTION_TIMEOUT_MS = 10000 // 10秒
- }
-
- // 协程相关
- private val job = Job()
- override val coroutineContext = Dispatchers.IO + job
-
- // 事件监听器列表
- private val eventListeners = mutableListOf()
- private val audioDataListeners = mutableListOf()
-
- // 内部音频播放器
- private val audioPlayer = BytedanceAudioPlayer(context)
-
- // 是否使用内部播放器
- private var useInternalPlayer = true
-
- // WebSocket连接
- private var webSocket: WebSocket? = null
- private val client: OkHttpClient
-
- // 会话信息
- private var sessionId = ""
- private var isConnected = false
- private var isSessionStarted = false
-
- // TTS配置参数
- private var appId: String = ""
- private var accessToken: String = ""
- private var speaker: String = "zh_female_yuanqinvyou_moon_bigtts" // 默认语音,使用Java示例的音色
- private var format: String = "pcm" // pcm 格式
- private var sampleRate: Int = 16000 // 使用16kHz采样率
- private var enableTimestamp: Boolean = false
- private var language: String = "zh-CN"
-
- // 流式处理状态
- private var currentStatus = STATUS_STOPPED
-
- // 连接相关状态
- private var connectionAttempts = 0
- private var isConnecting = AtomicBoolean(false)
- private var lastConnectionAttemptTime = 0L
-
- // 在类级别添加文本流通道
- private val textChannel = Channel(Channel.BUFFERED)
- private var textProcessingJob: Job? = null
-
- init {
- // 配置OkHttp客户端
- client = OkHttpClient.Builder()
- .pingInterval(50, TimeUnit.SECONDS) // 匹配Java示例设置50秒
- .readTimeout(100, TimeUnit.SECONDS) // 匹配Java示例设置100秒
- .writeTimeout(100, TimeUnit.SECONDS) // 匹配Java示例设置100秒
- .build()
-
- // 生成会话ID
- // sessionId = UUID.randomUUID().toString().replace("-", "")
- Log.d(TAG, "BytedanceTTS实例化完成!")
-
- // 初始化音频播放器回调
- initAudioPlayerCallbacks()
-
- // 启动文本处理协程
- startTextProcessing()
- }
-
- /**
- * 初始化音频播放器回调
- */
- private fun initAudioPlayerCallbacks() {
- audioPlayer.setOnPlayStarted {
- Log.d(TAG, "播放开始")
- updateStatus(STATUS_SPEAKING)
- notifyEvent(TtsEventType.PLAYBACK_STARTED)
- }
-
- // 设置播放完成回调
- audioPlayer.setOnPlayCompleted {
- Log.d(TAG, "播放结束")
- updateStatus(STATUS_READY)
- notifyEvent(TtsEventType.PLAYBACK_COMPLETED)
- }
-
- // 设置错误回调
- audioPlayer.setOnError { errorMsg ->
- updateStatus(STATUS_ERROR)
- notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "PLAYER_ERROR", "errorMessage" to errorMsg))
- }
-
- // 根据设置决定是否使用内部播放器
- updateInternalPlayerUsage()
- }
-
- /**
- * 启动文本处理协程
- */
- private fun startTextProcessing() {
- textProcessingJob?.cancel()
- textProcessingJob = launch {
- try {
- for (text in textChannel) {
- try {
- sendStreamTextRequest(text)
- } catch (e: Exception) {
- Log.e(TAG, "文本处理错误: ${e.message}")
- }
- }
- } catch (e: CancellationException) {
- // 协程被取消,正常情况
- } catch (e: Exception) {
- Log.e(TAG, "文本处理异常: ${e.message}")
- }
- }
- }
-
- /**
- * 停止语音合成
- */
- override fun stop(): Boolean {
- try {
- Log.i(TAG, ">stop()")
-
- if (useInternalPlayer) {
- audioPlayer.stop()
- }
- textProcessingJob?.cancel()
-
-
- if (isSessionStarted) {
- finishSession()
- }
- updateStatus(STATUS_STOPPED)
-
- return true
- } catch (e: Exception) {
- Log.e(TAG, "停止合成失败: ${e.message}")
- return false
- }
- }
-
- /**
- * 初始化火山语音服务
- */
- override fun initialize(
- ttsAppId: String,
- ttsAppToken: String,
- ttsResource: String,
- language: String
- ): Boolean {
- try {
- updateStatus(STATUS_INITIALIZING)
-
- // 设置基本参数
- this.appId = ttsAppId
- this.accessToken = ttsAppToken
- this.language = language
-
- // 重置连接状态
- connectionAttempts = 0
- isConnecting.set(false)
- lastConnectionAttemptTime = 0
-
- // 释放旧连接
- webSocket?.cancel()
- webSocket = null
- isConnected = false
- isSessionStarted = false
-
- return true
- } catch (e: Exception) {
- Log.e(TAG, "初始化失败: ${e.message}")
- updateStatus(STATUS_ERROR)
- return false
- }
- }
-
- /**
- * 重置服务状态
- */
- fun reset() {
- stop()
- textProcessingJob?.cancel()
-
- connectionAttempts = 0
- isConnecting.set(false)
- lastConnectionAttemptTime = 0
-
- webSocket?.cancel()
- webSocket = null
-
- isConnected = false
- isSessionStarted = false
-
- updateStatus(STATUS_STOPPED)
- startTextProcessing()
- }
-
- /**
- * 设置语音角色
- */
- override fun setVoice(voiceName: String): Boolean {
- try {
- speaker = voiceName
- return true
- } catch (e: Exception) {
- val errorMsg = e.message ?: "设置语音角色失败"
- notifyEvent(TtsEventType.ERROR, mapOf(
- "errorCode" to "VOICE_ERROR",
- "errorMessage" to errorMsg
- ))
- return false
- }
- }
-
- /**
- * 单次播放(非流式)
- */
- override fun speakOnce(text: String): Boolean {
- // 未实现:只支持流式模式
- Log.w(TAG, "speakOnce未实现,请使用speakStream")
- return false
- }
-
- /**
- * 流式播放开始
- */
- override fun speakStream(text: String): Boolean {
- try {
- if (text.isEmpty()) {
- return true
- }
-
- // 确保会话已就绪
- if (!isSessionStarted && !ensureSessionReady()) {
- Log.e(TAG, "流式播放失败:无法建立会话")
- return false
- }
-
- // 如果文本处理协程不活跃,重新启动它
- if (textProcessingJob == null || textProcessingJob?.isActive == false) {
- startTextProcessing()
- }
- // Log.d(TAG, "speakStream $sessionId, $text ")
-
- textChannel.trySend(text)
- return true
- } catch (e: Exception) {
- Log.e(TAG, "流式合成失败: ${e.message}")
- updateStatus(STATUS_ERROR)
- return false
- }
- }
-
- /**
- * 结束流式播放
- */
- override fun flushStream(): Boolean {
- try {
- // Log.d(TAG, "flushStream: $sessionId")
-
- if (isSessionStarted) {
- isSessionStarted = false
- finishSession()
- }
-
- return true
- } catch (e: Exception) {
- Log.e(TAG, "结束流式合成失败: ${e.message}")
- return false
- }
- }
-
- /**
- * 释放资源
- */
- override fun release() {
- Log.d(TAG, "BytedanceTTS release called.")
-
- // 关闭文本通道
- textChannel.close()
- textProcessingJob?.cancel()
-
- job.cancel()
-
- // 释放音频播放器资源
- audioPlayer.release()
-
- // 重置所有状态
- reset()
-
- // 确保WebSocket连接完全关闭
- webSocket?.cancel()
- webSocket = null
- }
-
- /**
- * 添加TTS事件监听器
- */
- override fun addListener(listener: TtsEventListener) {
- if (!eventListeners.contains(listener)) {
- eventListeners.add(listener)
- }
- }
-
- /**
- * 移除TTS事件监听器
- */
- override fun removeListener(listener: TtsEventListener) {
- eventListeners.remove(listener)
- }
-
- /**
- * 添加音频数据监听器
- */
- override fun addAudioDataListener(listener: AudioDataListener) {
- if (!audioDataListeners.contains(listener)) {
- audioDataListeners.add(listener)
- }
- }
-
- /**
- * 移除音频数据监听器
- */
- override fun removeAudioDataListener(listener: AudioDataListener) {
- audioDataListeners.remove(listener)
- }
-
- /**
- * 设置是否使用内部播放器
- */
- override fun setUseInternalPlayer(useInternalPlayer: Boolean) {
- this.useInternalPlayer = useInternalPlayer
- updateInternalPlayerUsage()
- }
-
- /**
- * 设置音频输出设备
- */
- override fun setAudioOutputDevice(device: AudioOutputDevice) : Boolean{
- if (useInternalPlayer) {
- audioPlayer.setAudioOutputDevice(device)
- }
- return true;
- }
-
- /**
- * 更新内部播放器的使用状态
- */
- private fun updateInternalPlayerUsage() {
- if (useInternalPlayer) {
- // 使用内部播放器
- if (!audioDataListeners.contains(audioPlayer)) {
- audioDataListeners.add(audioPlayer)
- }
- } else {
- // 不使用内部播放器
- audioDataListeners.remove(audioPlayer)
- }
- }
-
- /**
- * 通知事件处理
- */
- private fun notifyEvent(eventType: TtsEventType, params: Map = emptyMap()) {
- val event = TtsEvent(eventType, params)
- val listeners = ArrayList(eventListeners)
-
- for (listener in listeners) {
- try {
- listener.onEvent(event)
- } catch (e: Exception) {
- Log.e(TAG, "事件回调异常: ${e.message}")
- }
- }
-
- }
-
- /**
- * 通知音频数据
- */
- private fun notifyAudioData(data: ByteArray) {
- for (listener in audioDataListeners) {
- try {
- listener.onAudioData(data)
- } catch (e: Exception) {
- Log.e(TAG, "音频数据回调异常: ${e.message}")
- }
- }
-
- }
-
- /**
- * 确保会话已就绪
- */
- private fun ensureSessionReady(): Boolean {
- // 如果已连接和会话已开始,直接返回成功
- if (isConnected && isSessionStarted) {
- return true
- }
-
- // 如果已连接但会话未开始,只需要开始会话
- if (isConnected && !isSessionStarted) {
- // Log.d(TAG, "WebSocket已连接,正在启动新会话...")
- return startSessionOnly()
- }
-
- // 如果正在连接中,等待连接完成
- if (isConnecting.get()) {
- Log.d(TAG, "已有连接正在进行中,等待其完成...")
- // 等待连接完成,最多5秒
- var attempts = 0
- while (isConnecting.get() && attempts < 10) {
- try {
- Thread.sleep(500)
- attempts++
- } catch (e: InterruptedException) {
- return false
- }
- }
-
- // 连接完成后,检查是否需要启动会话
- if (isConnected && !isSessionStarted) {
- return startSessionOnly()
- }
-
- return isConnected
- }
-
- // 检查连接重试次数限制
- if (connectionAttempts >= MAX_CONNECTION_ATTEMPTS) {
- Log.e(TAG, "已达到最大连接尝试次数($MAX_CONNECTION_ATTEMPTS),不再尝试连接")
- return false
- }
-
- // 检查连接间隔时间 - 避免短时间内频繁重连
- val currentTime = System.currentTimeMillis()
- if (currentTime - lastConnectionAttemptTime < 2000 && lastConnectionAttemptTime > 0) {
- Log.w(TAG, "连接请求太频繁,等待冷却时间...")
- Thread.sleep(2000)
- }
-
- // 更新连接计数和时间
- connectionAttempts++
- lastConnectionAttemptTime = System.currentTimeMillis()
-
- // 尝试建立连接
- isConnecting.set(true)
- try {
- connect()
-
- // 等待连接建立,设置超时
- val startWaitTime = System.currentTimeMillis()
- while (!isConnected && System.currentTimeMillis() - startWaitTime < CONNECTION_TIMEOUT_MS) {
- try {
- Thread.sleep(500)
- // 如果连接已失败,提前退出等待
- if (!isConnecting.get() && !isConnected) {
- Log.e(TAG, "连接已失败,终止等待")
- break
- }
- } catch (e: InterruptedException) {
- isConnecting.set(false)
- return false
- }
- }
-
- // 连接成功后,启动会话
- if (isConnected && !isSessionStarted) {
- startSessionOnly()
- }
- } finally {
- isConnecting.set(false)
- }
-
- // 检查连接结果
- if (!isConnected) {
- Log.e(TAG, "连接超时: 无法在${CONNECTION_TIMEOUT_MS/1000}秒内建立连接,连接尝试次数: $connectionAttempts")
- return false
- }
-
- // 连接成功,重置尝试计数
- connectionAttempts = 0
- return true
- }
-
- /**
- * 只启动会话,不重新建立WebSocket连接
- */
- private fun startSessionOnly(): Boolean {
- if (!isConnected || webSocket == null) {
- Log.e(TAG, "无法启动会话:WebSocket未连接")
- return false
- }
-
- if (isSessionStarted) {
- return true
- }
-
- try {
- // 重新生成会话ID
- sessionId = UUID.randomUUID().toString().replace("-", "")
- Log.d(TAG, "启动新会话,会话ID: $sessionId")
-
- // 发送开始会话请求
- startTtsSession(webSocket!!)
-
- // 立即将会话视为已启动,不等待服务器确认
- isSessionStarted = true
- updateStatus(STATUS_READY)
-
- return true
- } catch (e: Exception) {
- Log.e(TAG, "启动会话失败: ${e.message}", e)
- return false
- }
- }
-
- /**
- * 更新状态并通知Dart层
- */
- private fun updateStatus(status: String) {
- currentStatus = status
- }
-
- /**
- * 建立WebSocket连接
- */
- private fun connect() {
- webSocket?.cancel()
- webSocket = null
-
- val url = "wss://openspeech.bytedance.com/api/v3/tts/bidirection"
-
- val connectId = UUID.randomUUID().toString()
- val request = Request.Builder()
- .url(url)
- .header("X-Api-App-Key", appId)
- .header("X-Api-Access-Key", accessToken)
- .header("X-Api-Resource-Id", "volc.service_type.10029")
- .header("X-Api-Connect-Id", connectId)
- .build()
-
- isConnected = false
- isSessionStarted = false
-
- webSocket = client.newWebSocket(request, object : WebSocketListener() {
- override fun onOpen(webSocket: WebSocket, response: Response) {
- isConnected = true
- startConnection(webSocket)
- }
-
- override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
- try {
- val data = bytes.toByteArray()
- val response = parseResponse(data)
-
- when (response.event) {
- EVENT_CONNECTION_FAILED, EVENT_SESSION_FAILED -> {
- val errorMsg = if (response.responseMetaJson.isNotEmpty()) {
- response.responseMetaJson
- } else {
- "连接或会话失败"
- }
- notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "TTS_ERROR",
- "errorMessage" to errorMsg))
- webSocket.close(1000, "Error")
- updateStatus(STATUS_ERROR)
- isSessionStarted = false
- isConnected = false
- isConnecting.set(false)
- }
-
- EVENT_SESSION_STARTED -> {
- connectionAttempts = 0
- // 会话开始时触发合成开始事件
- notifyEvent(TtsEventType.SYNTHESIS_STARTED)
- // 如果使用内部播放器则启动播放器会话
- if (useInternalPlayer) {
- audioPlayer.startSession()
- }
- }
-
- EVENT_TTS_RESPONSE -> {
- if (response.messageType == AUDIO_ONLY_RESPONSE && response.payload.isNotEmpty()) {
- notifyAudioData(response.payload)
- }
- }
-
- EVENT_CONNECTION_FINISHED -> {
- isConnected = false
- isSessionStarted = false
- isConnecting.set(false)
- webSocket.close(1000, "Connection finished")
- }
-
- EVENT_SESSION_FINISHED -> {
- isSessionStarted = false
- Log.i(TAG, "EVENT_SESSION_FINISHED, ${sessionId}")
-
- // 触发合成完成事件
- notifyEvent(TtsEventType.SYNTHESIS_COMPLETED)
-
- // 如果使用内部播放器则结束播放器会话
- if (useInternalPlayer) {
- audioPlayer.endSession()
- }
-
- // 流式模式下不自动关闭连接
- }
- }
- } catch (e: Exception) {
- Log.e(TAG, "解析响应失败: ${e.message}")
- updateStatus(STATUS_ERROR)
- isConnecting.set(false)
- }
- }
-
- override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
- isConnected = false
- isSessionStarted = false
- isConnecting.set(false)
-
- Log.e(TAG, "WebSocket连接失败: ${t.message}")
- notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "CONNECTION_ERROR",
- "errorMessage" to (t.message ?: "连接失败")))
- updateStatus(STATUS_ERROR)
- }
-
- override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
- isConnected = false
- isSessionStarted = false
- isConnecting.set(false)
- updateStatus(STATUS_STOPPED)
- notifyEvent(TtsEventType.SYNTHESIS_CANCELED)
- }
- })
- }
-
- /**
- * 开始连接
- */
- private fun startConnection(webSocket: WebSocket) {
- val header = Header(
- protocolVersion = PROTOCOL_VERSION,
- headerSize = DEFAULT_HEADER_SIZE,
- messageType = FULL_CLIENT_REQUEST,
- messageTypeSpecificFlags = MSG_TYPE_FLAG_WITH_EVENT,
- serializationMethod = JSON,
- messageCompression = COMPRESSION_NO,
- reserved = 0
- )
-
- val optional = Optional(
- event = EVENT_START_CONNECTION
- )
-
- val payload = "{}".toByteArray()
-
- sendEvent(webSocket, header, optional, payload)
- }
-
- /**
- * 结束连接
- */
- private fun finishConnection(webSocket: WebSocket) {
- val header = Header(
- protocolVersion = PROTOCOL_VERSION,
- headerSize = DEFAULT_HEADER_SIZE,
- messageType = FULL_CLIENT_REQUEST,
- messageTypeSpecificFlags = MSG_TYPE_FLAG_WITH_EVENT,
- serializationMethod = JSON,
- messageCompression = COMPRESSION_NO,
- reserved = 0
- )
-
- val optional = Optional(
- event = EVENT_FINISH_CONNECTION
- )
-
- val payload = "{}".toByteArray()
-
- sendEvent(webSocket, header, optional, payload)
- }
-
- /**
- * 开始TTS会话
- */
- private fun startTtsSession(webSocket: WebSocket) {
- val header = Header(
- protocolVersion = PROTOCOL_VERSION,
- headerSize = DEFAULT_HEADER_SIZE,
- messageType = FULL_CLIENT_REQUEST,
- messageTypeSpecificFlags = MSG_TYPE_FLAG_WITH_EVENT,
- serializationMethod = JSON,
- messageCompression = COMPRESSION_NO,
- reserved = 0
- )
-
- val optional = Optional(
- event = EVENT_START_SESSION,
- sessionId = sessionId
- )
-
- val jsonObject = JSONObject()
- val user = JSONObject()
- user.put("uid", "123456")
- jsonObject.put("user", user)
- jsonObject.put("event", EVENT_START_SESSION)
- jsonObject.put("namespace", "BidirectionalTTS")
-
- val reqParams = JSONObject()
- reqParams.put("speaker", speaker)
-
- val audioParams = JSONObject()
- audioParams.put("format", format)
- audioParams.put("sample_rate", sampleRate)
- audioParams.put("enable_timestamp", enableTimestamp)
-
- reqParams.put("audio_params", audioParams)
- jsonObject.put("req_params", reqParams)
-
- val payload = jsonObject.toString().toByteArray()
-
- sendEvent(webSocket, header, optional, payload)
- }
-
-
- /**
- * 发送文本进行合成 (流式)
- */
- private fun sendStreamTextRequest(text: String) {
- webSocket?.let { ws ->
- val header = Header(
- protocolVersion = PROTOCOL_VERSION,
- headerSize = DEFAULT_HEADER_SIZE,
- messageType = FULL_CLIENT_REQUEST,
- messageTypeSpecificFlags = MSG_TYPE_FLAG_WITH_EVENT,
- serializationMethod = JSON,
- messageCompression = COMPRESSION_NO,
- reserved = 0
- )
-
- val optional = Optional(
- event = EVENT_TASK_REQUEST,
- sessionId = sessionId
- )
-
- val jsonObject = JSONObject()
- val user = JSONObject()
- user.put("uid", "123456")
- jsonObject.put("user", user)
- jsonObject.put("event", EVENT_TASK_REQUEST)
- jsonObject.put("namespace", "BidirectionalTTS")
-
- val reqParams = JSONObject()
- // 将#和*替换为空格
- val processedText = text.replace("#", " ").replace("*", " ")
- reqParams.put("text", processedText)
- reqParams.put("speaker", speaker)
-
- val audioParams = JSONObject()
- audioParams.put("format", format)
- audioParams.put("sample_rate", sampleRate)
- audioParams.put("emotion", "happy")
-
- reqParams.put("audio_params", audioParams)
- jsonObject.put("req_params", reqParams)
-
- val payload = jsonObject.toString().toByteArray()
- // Log.d(TAG, "发送流式文本合成请求: ${jsonObject.toString()}")
-
- sendEvent(ws, header, optional, payload)
- // 流式模式下不自动结束会话
- }
- }
-
- /**
- * 结束会话
- */
- private fun finishSession() {
- webSocket?.let { ws ->
- val header = Header(
- protocolVersion = PROTOCOL_VERSION,
- headerSize = DEFAULT_HEADER_SIZE,
- messageType = FULL_CLIENT_REQUEST,
- messageTypeSpecificFlags = MSG_TYPE_FLAG_WITH_EVENT,
- serializationMethod = JSON,
- messageCompression = COMPRESSION_NO,
- reserved = 0
- )
-
- val optional = Optional(
- event = EVENT_FINISH_SESSION,
- sessionId = sessionId
- )
-
- val payload = "{}".toByteArray()
-
- sendEvent(ws, header, optional, payload)
- Log.d(TAG, "发送结束会话请求,会话ID: $sessionId")
- // 不在这里设置 isSessionStarted = false
- // 等待服务器的 SESSION_FINISHED 事件来设置
- }
- }
-
- /**
- * 发送事件
- */
- private fun sendEvent(webSocket: WebSocket, header: Header, optional: Optional, payload: ByteArray) {
- try {
- val headerBytes = header.getBytes()
- val optionalBytes = optional.getBytes()
- val payloadSizeBytes = intToBytes(payload.size)
-
- val requestBytes = ByteArray(
- headerBytes.size + optionalBytes.size + payloadSizeBytes.size + payload.size
- )
- var offset = 0
-
- System.arraycopy(headerBytes, 0, requestBytes, offset, headerBytes.size)
- offset += headerBytes.size
-
- System.arraycopy(optionalBytes, 0, requestBytes, offset, optionalBytes.size)
- offset += optionalBytes.size
-
- System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size)
- offset += payloadSizeBytes.size
-
- System.arraycopy(payload, 0, requestBytes, offset, payload.size)
-
- webSocket.send(ByteString.of(*requestBytes))
- } catch (e: Exception) {
- Log.e(TAG, "发送WebSocket消息失败: ${e.message}")
- }
- }
-
- /**
- * 解析响应
- */
- private fun parseResponse(data: ByteArray): TtsResponse {
- val response = TtsResponse()
-
- // 解析头部
- val header = Header()
- header.protocolVersion = (data[0].toInt() shr 4) and 0x0F
- header.headerSize = data[0].toInt() and 0x0F
- header.messageType = (data[1].toInt() shr 4) and 0x0F
- header.messageTypeSpecificFlags = data[1].toInt() and 0x0F
- header.serializationMethod = (data[2].toInt() shr 4) and 0x0F
- header.messageCompression = data[2].toInt() and 0x0F
- header.reserved = data[3].toInt()
-
- response.header = header
- response.messageType = header.messageType
-
- var offset = 4
-
- // 读取事件
- if (header.messageTypeSpecificFlags == MSG_TYPE_FLAG_WITH_EVENT) {
- val eventBytes = ByteArray(4)
- System.arraycopy(data, offset, eventBytes, 0, 4)
- response.event = bytesToInt(eventBytes)
- offset += 4
- }
-
- // 根据事件类型处理不同数据
- when (response.event) {
- EVENT_CONNECTION_STARTED -> {
- // 读取连接ID
- offset = readConnectId(data, response, offset)
- }
- EVENT_CONNECTION_FAILED -> {
- offset = readConnectId(data, response, offset)
- readMetaJson(data, response, offset)
- }
- EVENT_SESSION_STARTED,
- EVENT_TTS_RESPONSE,
- EVENT_TTS_SENTENCE_START,
- EVENT_TTS_SENTENCE_END -> {
- offset = readSessionId(data, response, offset)
- if (response.messageType == AUDIO_ONLY_RESPONSE) {
- offset = readPayload(data, response, offset)
- }
- }
- EVENT_SESSION_FAILED,
- EVENT_SESSION_FINISHED -> {
- offset = readSessionId(data, response, offset)
- readMetaJson(data, response, offset)
- }
- EVENT_CONNECTION_FINISHED -> {
- // 会话结束
- }
- }
-
- return response
- }
-
- /**
- * 读取连接ID
- */
- private fun readConnectId(data: ByteArray, response: TtsResponse, offset: Int): Int {
- val sizeBytes = ByteArray(4)
- System.arraycopy(data, offset, sizeBytes, 0, 4)
- val size = bytesToInt(sizeBytes)
-
- if (size > 0 && offset + 4 + size <= data.size) {
- val textBytes = ByteArray(size)
- System.arraycopy(data, offset + 4, textBytes, 0, size)
- response.connectionId = String(textBytes)
- }
-
- return offset + 4 + size
- }
-
- /**
- * 读取元数据JSON
- */
- private fun readMetaJson(data: ByteArray, response: TtsResponse, offset: Int): Int {
- val sizeBytes = ByteArray(4)
- System.arraycopy(data, offset, sizeBytes, 0, 4)
- val size = bytesToInt(sizeBytes)
-
- if (size > 0 && offset + 4 + size <= data.size) {
- val textBytes = ByteArray(size)
- System.arraycopy(data, offset + 4, textBytes, 0, size)
- response.responseMetaJson = String(textBytes)
- }
-
- return offset + 4 + size
- }
-
- /**
- * 读取会话ID
- */
- private fun readSessionId(data: ByteArray, response: TtsResponse, offset: Int): Int {
- val sizeBytes = ByteArray(4)
- System.arraycopy(data, offset, sizeBytes, 0, 4)
- val size = bytesToInt(sizeBytes)
-
- if (size > 0 && offset + 4 + size <= data.size) {
- val textBytes = ByteArray(size)
- System.arraycopy(data, offset + 4, textBytes, 0, size)
- response.sessionId = String(textBytes)
- }
-
- return offset + 4 + size
- }
-
- /**
- * 读取负载数据
- */
- private fun readPayload(data: ByteArray, response: TtsResponse, offset: Int): Int {
- val sizeBytes = ByteArray(4)
- System.arraycopy(data, offset, sizeBytes, 0, 4)
- val size = bytesToInt(sizeBytes)
-
- if (size > 0 && offset + 4 + size <= data.size) {
- val payload = ByteArray(size)
- System.arraycopy(data, offset + 4, payload, 0, size)
- response.payload = payload
- }
-
- return offset + 4 + size
- }
-
- /**
- * 整数转字节数组 (大端序)
- */
- private fun intToBytes(value: Int): ByteArray {
- return byteArrayOf(
- ((value shr 24) and 0xFF).toByte(),
- ((value shr 16) and 0xFF).toByte(),
- ((value shr 8) and 0xFF).toByte(),
- (value and 0xFF).toByte()
- )
- }
-
- /**
- * 字节数组转整数 (大端序)
- */
- private fun bytesToInt(bytes: ByteArray): Int {
- if (bytes.size != 4) {
- throw IllegalArgumentException("字节数组长度必须为4")
- }
-
- return ((bytes[0].toInt() and 0xFF) shl 24) or
- ((bytes[1].toInt() and 0xFF) shl 16) or
- ((bytes[2].toInt() and 0xFF) shl 8) or
- (bytes[3].toInt() and 0xFF)
- }
-
- /**
- * 消息头数据类
- */
- private data class Header(
- var protocolVersion: Int = PROTOCOL_VERSION,
- var headerSize: Int = DEFAULT_HEADER_SIZE,
- var messageType: Int = 0,
- var messageTypeSpecificFlags: Int = 0,
- var serializationMethod: Int = 0,
- var messageCompression: Int = 0,
- var reserved: Int = 0
- ) {
- /**
- * 转成字节数组
- */
- fun getBytes(): ByteArray {
- return byteArrayOf(
- ((protocolVersion shl 4) or headerSize).toByte(),
- ((messageType shl 4) or messageTypeSpecificFlags).toByte(),
- ((serializationMethod shl 4) or messageCompression).toByte(),
- reserved.toByte()
- )
- }
- }
-
- /**
- * 可选字段数据类
- */
- private data class Optional(
- var event: Int = EVENT_NONE,
- var sessionId: String = ""
- ) {
- /**
- * 转成字节数组
- */
- fun getBytes(): ByteArray {
- var bytes = ByteArray(0)
-
- // 添加事件字段
- if (event != EVENT_NONE) {
- bytes = intToBytes(event)
- }
-
- // 添加会话ID字段
- if (sessionId.isNotEmpty()) {
- val sessionIdBytes = sessionId.toByteArray()
- val sessionIdSizeBytes = intToBytes(sessionIdBytes.size)
-
- val temp = bytes
- val newBytes = ByteArray(temp.size + sessionIdSizeBytes.size + sessionIdBytes.size)
-
- var offset = 0
- System.arraycopy(temp, 0, newBytes, offset, temp.size)
- offset += temp.size
-
- System.arraycopy(sessionIdSizeBytes, 0, newBytes, offset, sessionIdSizeBytes.size)
- offset += sessionIdSizeBytes.size
-
- System.arraycopy(sessionIdBytes, 0, newBytes, offset, sessionIdBytes.size)
-
- bytes = newBytes
- }
-
- return bytes
- }
-
- /**
- * 整数转字节数组 (大端序)
- */
- private fun intToBytes(value: Int): ByteArray {
- return byteArrayOf(
- ((value shr 24) and 0xFF).toByte(),
- ((value shr 16) and 0xFF).toByte(),
- ((value shr 8) and 0xFF).toByte(),
- (value and 0xFF).toByte()
- )
- }
- }
-
- /**
- * TTS响应数据类
- */
- private data class TtsResponse(
- var header: Header = Header(),
- var event: Int = EVENT_NONE,
- var messageType: Int = 0,
- var sessionId: String = "",
- var connectionId: String = "",
- var responseMetaJson: String = "",
- var payload: ByteArray = ByteArray(0)
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as TtsResponse
-
- if (header != other.header) return false
- if (event != other.event) return false
- if (messageType != other.messageType) return false
- if (sessionId != other.sessionId) return false
- if (connectionId != other.connectionId) return false
- if (responseMetaJson != other.responseMetaJson) return false
- if (!payload.contentEquals(other.payload)) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = header.hashCode()
- result = 31 * result + event
- result = 31 * result + messageType
- result = 31 * result + sessionId.hashCode()
- result = 31 * result + connectionId.hashCode()
- result = 31 * result + responseMetaJson.hashCode()
- result = 31 * result + payload.contentHashCode()
- return result
- }
- }
-
-}
\ No newline at end of file
diff --git a/apps/client/local_plugins/bytedance_speech/lib/bytedance_speech.dart b/apps/client/local_plugins/bytedance_speech/lib/bytedance_speech.dart
deleted file mode 100644
index 522adb9b..00000000
--- a/apps/client/local_plugins/bytedance_speech/lib/bytedance_speech.dart
+++ /dev/null
@@ -1,12 +0,0 @@
-library bytedance_speech;
-
-import 'package:flutter/services.dart';
-
-class BytedanceSpeech {
- static const MethodChannel _channel = MethodChannel('bytedance_speech');
-
- static Future getPlatformVersion() async {
- final String? version = await _channel.invokeMethod('getPlatformVersion');
- return version;
- }
-}
\ No newline at end of file
diff --git a/apps/client/local_plugins/bytedance_speech/pubspec.yaml b/apps/client/local_plugins/bytedance_speech/pubspec.yaml
deleted file mode 100644
index 3bb55f40..00000000
--- a/apps/client/local_plugins/bytedance_speech/pubspec.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: bytedance_speech
-description: 火山引擎语音识别和语音合成Flutter插件
-version: 0.0.1
-homepage:
-
-environment:
- sdk: ">=2.19.0 <4.0.0"
- flutter: ">=3.0.0"
-
-dependencies:
- flutter:
- sdk: flutter
-
-dev_dependencies:
- flutter_test:
- sdk: flutter
- flutter_lints: ^2.0.0
-
-flutter:
- plugin:
- platforms:
- android:
- package: com.deep_voice.bytedance_speech
- pluginClass: BytedanceSpeechPlugin
- ios:
- pluginClass: BytedanceSpeechPlugin
\ No newline at end of file
diff --git a/apps/client/local_plugins/device_jieli/android/.gitignore b/apps/client/local_plugins/device_jieli/android/.gitignore
deleted file mode 100644
index 161bdcda..00000000
--- a/apps/client/local_plugins/device_jieli/android/.gitignore
+++ /dev/null
@@ -1,9 +0,0 @@
-*.iml
-.gradle
-/local.properties
-/.idea/workspace.xml
-/.idea/libraries
-.DS_Store
-/build
-/captures
-.cxx
diff --git a/apps/client/local_plugins/device_jieli/android/build.gradle b/apps/client/local_plugins/device_jieli/android/build.gradle
deleted file mode 100644
index 9756badd..00000000
--- a/apps/client/local_plugins/device_jieli/android/build.gradle
+++ /dev/null
@@ -1,80 +0,0 @@
-group = "com.jielihome.jielihome"
-version = "1.0-SNAPSHOT"
-
-buildscript {
- ext.kotlin_version = "2.1.0"
- repositories {
- google()
- mavenCentral()
- }
-
- dependencies {
- classpath("com.android.tools.build:gradle:8.7.3")
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
- }
-}
-
-allprojects {
- repositories {
- google()
- mavenCentral()
- flatDir {
- dirs "libs"
- }
- }
-}
-
-apply plugin: "com.android.library"
-apply plugin: "kotlin-android"
-
-android {
- namespace = "com.jielihome.jielihome"
-
- compileSdk = 36
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
-
- kotlinOptions {
- jvmTarget = JavaVersion.VERSION_17
- }
-
- sourceSets {
- main.java.srcDirs += "src/main/kotlin"
- test.java.srcDirs += "src/test/kotlin"
- }
-
- defaultConfig {
- minSdk = 21
- }
-
- dependencies {
- // The JieLi AARs are compile-only here. The host app must redeclare them
- // as runtime dependencies in its own build.gradle (see example/android/app).
- compileOnly fileTree(dir: 'libs', include: ['*.aar', '*.jar'])
- // device_plugin_interface 提供 DeviceCallTranslationPort 等通用接口,
- // 让 JieliCallTranslationPort 可以无 vendor 耦合地被 translate_server 消费。
- implementation project(':device_plugin_interface')
- implementation "com.squareup.okhttp3:okhttp:4.12.0"
- implementation "com.google.code.gson:gson:2.10.1"
- implementation "androidx.core:core-ktx:1.13.1"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
-
- testImplementation("org.jetbrains.kotlin:kotlin-test")
- testImplementation("org.mockito:mockito-core:5.0.0")
- }
-
- testOptions {
- unitTests.all {
- useJUnitPlatform()
-
- testLogging {
- events "passed", "skipped", "failed", "standardOut", "standardError"
- outputs.upToDateWhen {false}
- showStandardStreams = true
- }
- }
- }
-}
diff --git a/apps/client/local_plugins/device_jieli/android/libs/jl_audio_decode_V2.1.0_20012-release.aar b/apps/client/local_plugins/device_jieli/android/libs/jl_audio_decode_V2.1.0_20012-release.aar
deleted file mode 100644
index 1a609561..00000000
Binary files a/apps/client/local_plugins/device_jieli/android/libs/jl_audio_decode_V2.1.0_20012-release.aar and /dev/null differ
diff --git a/apps/client/local_plugins/device_jieli/android/libs/jl_audio_v2_V1.0.0_9-release.aar b/apps/client/local_plugins/device_jieli/android/libs/jl_audio_v2_V1.0.0_9-release.aar
deleted file mode 100644
index 9bfae54a..00000000
Binary files a/apps/client/local_plugins/device_jieli/android/libs/jl_audio_v2_V1.0.0_9-release.aar and /dev/null differ
diff --git a/apps/client/local_plugins/device_jieli/android/libs/jl_bluetooth_rcsp_V4.2.0_beta1_40255_20260324.aar b/apps/client/local_plugins/device_jieli/android/libs/jl_bluetooth_rcsp_V4.2.0_beta1_40255_20260324.aar
deleted file mode 100644
index 0ff72f1c..00000000
Binary files a/apps/client/local_plugins/device_jieli/android/libs/jl_bluetooth_rcsp_V4.2.0_beta1_40255_20260324.aar and /dev/null differ
diff --git a/apps/client/local_plugins/device_jieli/android/libs/jldecryption_v0.4-release.aar b/apps/client/local_plugins/device_jieli/android/libs/jldecryption_v0.4-release.aar
deleted file mode 100644
index fd933770..00000000
Binary files a/apps/client/local_plugins/device_jieli/android/libs/jldecryption_v0.4-release.aar and /dev/null differ
diff --git a/apps/client/local_plugins/device_jieli/android/settings.gradle b/apps/client/local_plugins/device_jieli/android/settings.gradle
deleted file mode 100644
index 8d18be72..00000000
--- a/apps/client/local_plugins/device_jieli/android/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = 'jielihome'
diff --git a/apps/client/local_plugins/device_jieli/android/src/main/AndroidManifest.xml b/apps/client/local_plugins/device_jieli/android/src/main/AndroidManifest.xml
deleted file mode 100644
index 368a6627..00000000
--- a/apps/client/local_plugins/device_jieli/android/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/JielihomePlugin.kt b/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/JielihomePlugin.kt
deleted file mode 100644
index 4828c7db..00000000
--- a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/JielihomePlugin.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.jielihome.jielihome
-
-import com.jielihome.jielihome.bridge.MethodRouter
-import com.jielihome.jielihome.core.JieliHomeServer
-import com.jielihome.jielihome.integration.JieliNativeDevicePlugin
-import io.flutter.embedding.engine.plugins.FlutterPlugin
-import io.flutter.plugin.common.EventChannel
-import io.flutter.plugin.common.MethodChannel
-
-/**
- * Flutter 入口,仅负责注册 Channel 并把请求转交给 MethodRouter。
- * 全部业务逻辑在 [JieliHomeServer] 与 feature/event 子模块中。
- */
-class JielihomePlugin : FlutterPlugin {
-
- private lateinit var methodChannel: MethodChannel
- private lateinit var eventChannel: EventChannel
- private lateinit var router: MethodRouter
-
- override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
- val server = JieliHomeServer.get()
- router = MethodRouter(binding.applicationContext, server)
-
- methodChannel = MethodChannel(binding.binaryMessenger, "device_jieli/method")
- methodChannel.setMethodCallHandler(router)
-
- eventChannel = EventChannel(binding.binaryMessenger, "device_jieli/event")
- eventChannel.setStreamHandler(server.dispatcher)
-
- // 把杰理 vendor 注册到 device_manager 的 NativeDevicePluginRegistry:
- // 这样 app 走 device_manager 路径时能 listVendors / useVendor("jieli")。
- // SDK 直连路径(device_jieli/method 上面那条)保留不变;两条路径并存。
- JieliNativeDevicePlugin.register(binding.applicationContext)
- }
-
- override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
- methodChannel.setMethodCallHandler(null)
- eventChannel.setStreamHandler(null)
- // 进程级资源不在这里 shutdown,保持后续 attach 仍可继续使用
- }
-}
diff --git a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/api/JieliEventListener.kt b/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/api/JieliEventListener.kt
deleted file mode 100644
index 908960f6..00000000
--- a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/api/JieliEventListener.kt
+++ /dev/null
@@ -1,175 +0,0 @@
-package com.jielihome.jielihome.api
-
-/**
- * 原生层事件监听器。宿主 App 在 Activity/Service 里实现并注册到
- * [com.jielihome.jielihome.core.JieliHomeServer.addEventListener],
- * 即可订阅插件所有事件而不依赖 Flutter EventChannel。
- *
- * 所有回调里的 payload 都是 [Map],字段与 Dart 侧 EventChannel 推上去的完全一致,
- * 因此原生订阅与 Dart 订阅可以并存(fan-out)。
- *
- * 默认提供「按 type 路由」的便捷扩展,子类只覆盖关心的方法即可。
- */
-interface JieliEventListener {
- /** 任意事件,原始 payload。可以自己按 payload["type"] 路由。 */
- fun onEvent(payload: Map) {}
-}
-
-/**
- * 按事件类型分发的便捷适配器。子类只覆盖关心的回调,未覆盖的事件会落到 [onUnknown]。
- */
-abstract class JieliEventAdapter : JieliEventListener {
-
- override fun onEvent(payload: Map) {
- when (payload["type"] as? String) {
- "adapterStatus" -> onAdapterStatus(
- enabled = payload["enabled"] as? Boolean ?: false,
- hasBle = payload["hasBle"] as? Boolean ?: false,
- )
-
- "scanStatus" -> onScanStatus(
- ble = payload["ble"] as? Boolean ?: false,
- started = payload["started"] as? Boolean ?: false,
- )
-
- "deviceFound" -> onDeviceFound(payload)
- "bondStatus" -> onBondStatus(
- address = payload["address"] as? String ?: "",
- status = payload["status"] as? Int ?: 0,
- )
- "rcspInit" -> onRcspInit(
- address = payload["address"] as? String ?: "",
- code = payload["code"] as? Int ?: -1,
- )
- "connectionState" -> onConnectionState(
- address = payload["address"] as? String ?: "",
- state = payload["state"] as? Int ?: -1,
- )
-
- "battery" -> onBattery(
- address = payload["address"] as? String,
- level = payload["level"] as? Int,
- )
- "phoneCallStatus" -> onPhoneCallStatus(
- address = payload["address"] as? String,
- status = payload["status"] as? Int ?: 0,
- )
- "voiceMode" -> onVoiceMode(
- address = payload["address"] as? String,
- modeId = payload["modeId"] as? Int,
- )
-
- "volume" -> onVolume(payload)
- "musicName" -> onMusicName(
- address = payload["address"] as? String,
- name = payload["name"] as? String,
- )
- "musicStatus" -> onMusicStatus(payload)
- "playMode" -> onPlayMode(
- address = payload["address"] as? String,
- mode = payload["mode"] as? Int,
- )
-
- "expandFunction" -> onExpandFunction(payload)
-
- "translationAudio" -> {
- onTranslationAudio(payload)
- onTranslationEvent(payload)
- }
- "translationResult" -> {
- onTranslationResult(payload)
- onTranslationEvent(payload)
- }
- "translationError" -> {
- onTranslationError(payload)
- onTranslationEvent(payload)
- }
- "translationLog" -> {
- onTranslationLog(payload)
- onTranslationEvent(payload)
- }
-
- "speechStart" -> onSpeechStart(payload)
- "speechAudio" -> onSpeechAudio(payload)
- "speechEnd" -> onSpeechEnd(payload)
- "speechError" -> onSpeechError(payload)
-
- "deviceRecordStart" -> onDeviceRecordStart(payload)
- "deviceRecordAudio" -> onDeviceRecordAudio(payload)
- "deviceRecordStop" -> onDeviceRecordStop(payload)
- "deviceRecordError" -> onDeviceRecordError(payload)
-
- "otaState" -> onOtaState(payload)
- "otaError" -> onOtaError(payload)
-
- else -> onUnknown(payload)
- }
- }
-
- open fun onAdapterStatus(enabled: Boolean, hasBle: Boolean) {}
- open fun onScanStatus(ble: Boolean, started: Boolean) {}
- open fun onDeviceFound(payload: Map) {}
- open fun onBondStatus(address: String, status: Int) {}
- open fun onRcspInit(address: String, code: Int) {}
- open fun onConnectionState(address: String, state: Int) {}
-
- open fun onBattery(address: String?, level: Int?) {}
- open fun onPhoneCallStatus(address: String?, status: Int) {}
- open fun onVoiceMode(address: String?, modeId: Int?) {}
-
- open fun onVolume(payload: Map) {}
- open fun onMusicName(address: String?, name: String?) {}
- open fun onMusicStatus(payload: Map) {}
- open fun onPlayMode(address: String?, mode: Int?) {}
-
- open fun onExpandFunction(payload: Map) {}
-
- /** 翻译事件统一入口(兜底,默认细分回调之后还会触发一次) */
- open fun onTranslationEvent(payload: Map) {}
-
- /** payload["pcm"] 为 ByteArray;附带 streamId/sampleRate/seq/tsMs/final 字段 */
- open fun onTranslationAudio(payload: Map) {}
-
- /** payload 含 srcLang/srcText/destLang/destText/requestId */
- open fun onTranslationResult(payload: Map) {}
-
- /** payload 含 code/message */
- open fun onTranslationError(payload: Map) {}
-
- open fun onTranslationLog(payload: Map) {}
-
- /** 耳机检测到唤醒/语音助手触发;payload: voiceType, sampleRate, vadWay, address, tsMs */
- open fun onSpeechStart(payload: Map) {}
-
- /** 语音助手音频帧;payload: pcm(ByteArray), encoding, sampleRate, channels, address */
- open fun onSpeechAudio(payload: Map) {}
-
- /** 语音助手会话结束;payload: reason, message, address */
- open fun onSpeechEnd(payload: Map) {}
-
- open fun onSpeechError(payload: Map) {}
-
- // ── 设备录音 ────────────────────────────────────────────────────────────
-
- /** 录音上行已启动;payload: address, sampleRate, tsMs */
- open fun onDeviceRecordStart(payload: Map) {}
-
- /**
- * 录音音频帧;payload: address, streamId("in.uplink"/"in.downlink"),
- * pcm(ByteArray), sampleRate, channels, bitsPerSample, tsMs
- */
- open fun onDeviceRecordAudio(payload: Map) {}
-
- /** 录音上行已停止;payload: address, tsMs */
- open fun onDeviceRecordStop(payload: Map) {}
-
- /** 录音错误;payload: address, code, message */
- open fun onDeviceRecordError(payload: Map) {}
-
- /** OTA 进度/状态:state(String), sent(Long), total(Long), percent(Int) */
- open fun onOtaState(payload: Map) {}
-
- open fun onOtaError(payload: Map) {}
-
- open fun onUnknown(payload: Map) {}
-}
diff --git a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/LocalPlayer.kt b/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/LocalPlayer.kt
deleted file mode 100644
index 01e9cfd3..00000000
--- a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/LocalPlayer.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.jielihome.jielihome.audio
-
-import android.media.AudioAttributes
-import android.media.AudioFormat
-import android.media.AudioManager
-import android.media.AudioTrack
-import java.util.concurrent.atomic.AtomicBoolean
-
-/**
- * 本地 PCM 播放(A2DP 路径)。用于音视频翻译模式:TTS 不回耳机,直接走 A2DP。
- */
-class LocalPlayer(
- private val sampleRate: Int = 16000,
- private val channels: Int = 1,
-) {
- private var track: AudioTrack? = null
- private val playing = AtomicBoolean(false)
-
- fun start() {
- if (playing.get()) return
- val cfg = if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO
- val minBuf = AudioTrack.getMinBufferSize(sampleRate, cfg, AudioFormat.ENCODING_PCM_16BIT)
-
- track = AudioTrack.Builder()
- .setAudioAttributes(
- AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_MEDIA)
- .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
- .build()
- )
- .setAudioFormat(
- AudioFormat.Builder()
- .setSampleRate(sampleRate)
- .setChannelMask(cfg)
- .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
- .build()
- )
- .setBufferSizeInBytes(maxOf(minBuf, sampleRate * 2 * channels)) // 1s
- .setTransferMode(AudioTrack.MODE_STREAM)
- .build()
- track?.play()
- playing.set(true)
- }
-
- fun feed(pcm: ByteArray) {
- if (!playing.get()) return
- runCatching { track?.write(pcm, 0, pcm.size) }
- }
-
- fun stop() {
- if (!playing.compareAndSet(true, false)) return
- runCatching { track?.stop() }
- runCatching { track?.release() }
- track = null
- }
-
- @Suppress("unused")
- fun setVolumeFollowingSystemMedia() {
- // hint: 用 AudioManager.STREAM_MUSIC 已由 USAGE_MEDIA 隐式选择
- @Suppress("DEPRECATION")
- AudioManager.STREAM_MUSIC // no-op
- }
-}
diff --git a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/OpusStreamCodec.kt b/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/OpusStreamCodec.kt
deleted file mode 100644
index 370357ba..00000000
--- a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/OpusStreamCodec.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-package com.jielihome.jielihome.audio
-
-import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback
-import com.jieli.jl_audio_decode.callback.OnEncodeStreamCallback
-import com.jieli.jl_audio_decode.opus.OpusManager
-import com.jieli.jl_audio_decode.opus.model.OpusOption
-
-/**
- * OPUS 流式解码器。
- * 输入:从耳机收到的 OPUS 编码帧
- * 输出:16kHz / mono PCM 流
- *
- * **packetSize 是单个 OPUS 帧的实际字节大小,不是"传输块大小"**。
- * 16k/16bit/mono/20ms ≈ 40B/帧,stereo ≈ 80B/帧。
- * 杰理 SDK demo 里用 200/80 这类"块"值会让 OpusManager 把多帧拼包当成 1 帧解,
- * 直接丢掉后面的帧(mono 200B = 5 帧 → 只解 1 帧,丢 80%)。
- */
-class OpusStreamDecoder(
- private val channel: Int = 1,
- private val packetSize: Int = 200,
- private val sampleRate: Int = 16000,
- private val hasHead: Boolean = false,
- private val onPcm: (pcm: ByteArray) -> Unit,
- private val onError: (code: Int, msg: String?) -> Unit = { _, _ -> },
-) {
- private val manager = OpusManager()
-
- @Volatile
- private var started = false
-
- fun start() {
- if (started) return
- val option = OpusOption()
- .setHasHead(hasHead)
- .setChannel(channel)
- .setPacketSize(packetSize)
- .setSampleRate(sampleRate)
- manager.startDecodeStream(option, object : OnDecodeStreamCallback {
- override fun onDecodeStream(pcm: ByteArray) { runCatching { onPcm(pcm) } }
- override fun onStart() {}
- override fun onComplete(p0: String?) {}
- override fun onError(code: Int, msg: String?) {
- runCatching { onError(code, msg) }
- }
- })
- started = true
- }
-
- fun feedEncoded(opus: ByteArray) {
- if (!started) return
- runCatching { manager.writeAudioStream(opus) }
- }
-
- fun stop() {
- if (!started) return
- runCatching { manager.stopDecodeStream() }
- runCatching { manager.release() }
- started = false
- }
-}
-
-/**
- * OPUS 流式编码器。把 PCM 编码成 OPUS 帧后回调 [onOpus],由调用方包成 AudioData 回送给设备。
- */
-class OpusStreamEncoder(
- private val channel: Int = 1,
- private val packetSize: Int = 200,
- private val sampleRate: Int = 16000,
- private val hasHead: Boolean = false,
- private val onOpus: (opus: ByteArray) -> Unit,
- private val onError: (code: Int, msg: String?) -> Unit = { _, _ -> },
-) {
- private val manager = OpusManager()
-
- @Volatile
- private var started = false
-
- fun start() {
- if (started) return
- val option = OpusOption()
- .setHasHead(hasHead)
- .setChannel(channel)
- .setPacketSize(packetSize)
- .setSampleRate(sampleRate)
- manager.startEncodeStream(option, object : OnEncodeStreamCallback {
- override fun onEncodeStream(opus: ByteArray) { runCatching { onOpus(opus) } }
- override fun onStart() {}
- override fun onComplete(p0: String?) {}
- override fun onError(code: Int, msg: String?) {
- runCatching { onError(code, msg) }
- }
- })
- started = true
- }
-
- fun feedPcm(pcm: ByteArray) {
- if (!started) return
- runCatching { manager.writeEncodeStream(pcm) }
- }
-
- fun stop() {
- if (!started) return
- runCatching { manager.stopEncodeStream() }
- runCatching { manager.release() }
- started = false
- }
-}
diff --git a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/PcmKit.kt b/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/PcmKit.kt
deleted file mode 100644
index acf037f0..00000000
--- a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/PcmKit.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.jielihome.jielihome.audio
-
-object PcmKit {
- /**
- * 把 16bit 立体声 PCM 拆为左/右两路单声道。
- * 入参字节流:L0_lo L0_hi R0_lo R0_hi L1_lo L1_hi R1_lo R1_hi ...
- * 返回:Pair(leftMono, rightMono)
- */
- fun splitStereo16(pcm: ByteArray): Pair {
- val frames = pcm.size / 4
- val left = ByteArray(frames * 2)
- val right = ByteArray(frames * 2)
- var li = 0
- var ri = 0
- var i = 0
- while (i + 3 < pcm.size) {
- left[li++] = pcm[i]
- left[li++] = pcm[i + 1]
- right[ri++] = pcm[i + 2]
- right[ri++] = pcm[i + 3]
- i += 4
- }
- return left to right
- }
-}
diff --git a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/PhoneMicCapture.kt b/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/PhoneMicCapture.kt
deleted file mode 100644
index c602a866..00000000
--- a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/audio/PhoneMicCapture.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.jielihome.jielihome.audio
-
-import android.Manifest
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.pm.PackageManager
-import android.media.AudioFormat
-import android.media.AudioRecord
-import android.media.MediaRecorder
-import android.util.Log
-import androidx.core.content.ContextCompat
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.concurrent.thread
-
-/**
- * 手机麦克风采集,输出 16kHz / 16bit / mono PCM。
- * 每 20ms 切一帧(320 samples = 640 bytes)。
- *
- * 用法:
- * val mic = PhoneMicCapture(context) { pcm -> /* push to bridge */ }
- * mic.start()
- * ...
- * mic.stop()
- */
-class PhoneMicCapture(
- private val context: Context,
- private val sampleRate: Int = 16000,
- private val frameDurationMs: Int = 20,
- private val onFrame: (pcm: ByteArray) -> Unit,
- private val onError: (code: Int, msg: String) -> Unit = { _, _ -> },
-) {
- private val running = AtomicBoolean(false)
- private var recorder: AudioRecord? = null
- private var worker: Thread? = null
-
- val frameSizeBytes: Int = sampleRate * frameDurationMs / 1000 * 2 /* 16bit */
-
- @SuppressLint("MissingPermission")
- fun start(): Boolean {
- if (running.get()) return true
- if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
- != PackageManager.PERMISSION_GRANTED
- ) {
- Log.w(TAG, "RECORD_AUDIO permission missing")
- onError(-101, "RECORD_AUDIO permission missing")
- return false
- }
-
- val minBuf = AudioRecord.getMinBufferSize(
- sampleRate,
- AudioFormat.CHANNEL_IN_MONO,
- AudioFormat.ENCODING_PCM_16BIT
- )
- val bufSize = maxOf(minBuf, frameSizeBytes * 4)
-
- val ar = AudioRecord(
- MediaRecorder.AudioSource.VOICE_COMMUNICATION,
- sampleRate,
- AudioFormat.CHANNEL_IN_MONO,
- AudioFormat.ENCODING_PCM_16BIT,
- bufSize
- )
- if (ar.state != AudioRecord.STATE_INITIALIZED) {
- ar.release()
- Log.w(TAG, "AudioRecord init failed")
- onError(-102, "AudioRecord init failed (state=${ar.state})")
- return false
- }
- recorder = ar
- running.set(true)
- ar.startRecording()
-
- worker = thread(name = "device-jieli-mic", isDaemon = true) {
- val frame = ByteArray(frameSizeBytes)
- while (running.get()) {
- val n = try { ar.read(frame, 0, frameSizeBytes) } catch (t: Throwable) {
- onError(-103, "AudioRecord.read crashed: ${t.message}"); -1
- }
- when {
- n < 0 -> { onError(-104, "AudioRecord.read err code=$n"); break }
- n == 0 -> continue
- n == frameSizeBytes -> onFrame(frame.copyOf())
- else -> onFrame(frame.copyOf(n))
- }
- }
- }
- return true
- }
-
- fun stop() {
- if (!running.compareAndSet(true, false)) return
- runCatching { recorder?.stop() }
- runCatching { recorder?.release() }
- recorder = null
- worker?.join(200)
- worker = null
- }
-
- companion object {
- private const val TAG = "PhoneMicCapture"
- }
-}
diff --git a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/bridge/EventDispatcher.kt b/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/bridge/EventDispatcher.kt
deleted file mode 100644
index 779c2e53..00000000
--- a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/bridge/EventDispatcher.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.jielihome.jielihome.bridge
-
-import android.os.Handler
-import android.os.Looper
-import com.jielihome.jielihome.api.JieliEventListener
-import io.flutter.plugin.common.EventChannel
-import java.util.concurrent.CopyOnWriteArrayList
-
-/**
- * 事件总线。所有 forwarder / feature 调用 [send],由本类做 fan-out:
- * - Flutter 端:通过 EventChannel.EventSink 推到 Dart
- * - 原生端:通过 [JieliEventListener] 推给宿主直接订阅
- *
- * Flutter / 原生 / 多个原生监听器 可以同时存在;payload 形态完全一致。
- */
-class EventDispatcher : EventChannel.StreamHandler {
-
- private val mainHandler = Handler(Looper.getMainLooper())
-
- @Volatile
- private var sink: EventChannel.EventSink? = null
-
- private val nativeListeners = CopyOnWriteArrayList()
-
- override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
- sink = events
- }
-
- override fun onCancel(arguments: Any?) {
- sink = null
- }
-
- fun addNativeListener(listener: JieliEventListener) {
- if (!nativeListeners.contains(listener)) nativeListeners.add(listener)
- }
-
- fun removeNativeListener(listener: JieliEventListener) {
- nativeListeners.remove(listener)
- }
-
- fun send(payload: Map) {
- // 原生监听同步派发(在事件原始线程,不经主线程切换,方便高频音频帧)
- for (l in nativeListeners) {
- try { l.onEvent(payload) } catch (_: Throwable) { /* swallow listener errors */ }
- }
- // Flutter 端必须在主线程
- val s = sink ?: return
- mainHandler.post { runCatching { s.success(payload) } }
- }
-}
diff --git a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/bridge/MethodRouter.kt b/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/bridge/MethodRouter.kt
deleted file mode 100644
index 67c532de..00000000
--- a/apps/client/local_plugins/device_jieli/android/src/main/kotlin/com/jielihome/jielihome/bridge/MethodRouter.kt
+++ /dev/null
@@ -1,270 +0,0 @@
-package com.jielihome.jielihome.bridge
-
-import android.content.Context
-import com.jielihome.jielihome.core.JieliHomeServer
-import io.flutter.plugin.common.MethodCall
-import io.flutter.plugin.common.MethodChannel
-
-/**
- * MethodChannel 路由层:把 Dart 调用映射到 server 内的 feature 模块。
- * Plugin 类不直接处理任何业务逻辑,只装配 router。
- */
-class MethodRouter(
- private val context: Context,
- private val server: JieliHomeServer,
-) : MethodChannel.MethodCallHandler {
-
- override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
- try {
- when (call.method) {
- "getPlatformVersion" ->
- result.success("Android ${android.os.Build.VERSION.RELEASE}")
-
- "initialize" -> {
- server.initialize(
- context = context,
- multiDevice = call.argument("multiDevice") ?: true,
- skipNoNameDev = call.argument("skipNoNameDev") ?: false,
- enableLog = call.argument("enableLog") ?: false,
- )
- result.success(true)
- }
-
- "startScan" -> {
- val timeout = call.argument("timeoutMs") ?: 30000
- val nameList = call.argument>("nameList") ?: emptyList()
- val uuidList = call.argument>("uuidList") ?: emptyList()
- val skipUnnamed = call.argument("skipUnnamed") ?: true
- server.scanFeature.startScan(timeout, nameList, uuidList, skipUnnamed)
- .fold({ result.success(true) },
- { result.error("SCAN_FAILED", it.message, null) })
- }
-
- "stopScan" -> { server.scanFeature.stopScan(); result.success(true) }
-
- "isScanning" -> result.success(server.scanFeature.isScanning())
-
- "connect" -> {
- val mac = call.argument("address")
- ?: return result.error("BAD_ARG", "address required", null)
- server.connectFeature.connect(
- bleAddress = mac,
- edrAddress = call.argument("edrAddr"),
- deviceType = call.argument("deviceType") ?: -1,
- connectWay = call.argument("connectWay") ?: 0,
- ).fold({ result.success(true) },
- { result.error("CONNECT_FAILED", it.message, null) })
- }
-
- "disconnect" -> {
- val mac = call.argument("address")
- ?: return result.error("BAD_ARG", "address required", null)
- server.connectFeature.disconnect(mac)
- .fold({ result.success(true) },
- { result.error("DISCONNECT_FAILED", it.message, null) })
- }
-
- "isConnected" -> {
- val mac = call.argument("address")
- ?: return result.error("BAD_ARG", "address required", null)
- result.success(server.connectFeature.isConnected(mac))
- }
-
- "connectedDevice" -> {
- val d = server.connectFeature.connectedDevice()
- if (d == null) result.success(null)
- else result.success(
- mapOf(
- "address" to d.address,
- "name" to runCatching { d.name }.getOrNull()
- )
- )
- }
-
- "deviceSnapshot" -> {
- val mac = call.argument("address")
- ?: return result.error("BAD_ARG", "address required", null)
- result.success(server.deviceInfoFeature.snapshot(mac))
- }
-
- "queryTargetInfo" -> {
- val mac = call.argument("address")
- ?: return result.error("BAD_ARG", "address required", null)
- val mask = call.argument("mask") ?: 0x0F
- server.deviceInfoFeature.queryTargetInfo(
- mac, mask,
- onSuccess = { result.success(it) },
- onError = { c, m -> result.error("TARGET_INFO_ERR", m, c) }
- )
- }
-
- "sendCustomCmd" -> {
- val mac = call.argument("address")
- ?: return result.error("BAD_ARG", "address required", null)
- val opCode = call.argument("opCode")
- ?: return result.error("BAD_ARG", "opCode required", null)
- val payload = call.argument("payload") ?: ByteArray(0)
- server.customCmdFeature.send(
- mac, opCode, payload,
- onSuccess = { result.success(it) },
- onError = { c, m -> result.error("CUSTOM_CMD_ERR", m, c) }
- )
- }
-
- "startTranslation" -> {
- val modeId = call.argument("modeId")
- ?: return result.error("BAD_ARG", "modeId required", null)
- @Suppress("UNCHECKED_CAST")
- val args = (call.argument