Browse Source

Merge commit 'a96995f84abd2b27649088693ce0ae9db897d3e3' into lxm

weicu
lxm 1 year ago
parent
commit
73cecd0e0f
  1. 103
      .cursorrules
  2. 2
      .gitignore
  3. 10
      .mcp copy.json
  4. 10
      .mcp.json
  5. 33
      amap_flutter/.gitignore
  6. 33
      amap_flutter/.metadata
  7. 3
      amap_flutter/CHANGELOG.md
  8. 1
      amap_flutter/LICENSE
  9. 15
      amap_flutter/README.md
  10. 4
      amap_flutter/analysis_options.yaml
  11. 9
      amap_flutter/android/.gitignore
  12. 54
      amap_flutter/android/build.gradle
  13. 1
      amap_flutter/android/settings.gradle
  14. 3
      amap_flutter/android/src/main/AndroidManifest.xml
  15. 169
      amap_flutter/android/src/main/java/com/example/amap_flutter/AmapFlutterPlugin.java
  16. 38
      amap_flutter/ios/.gitignore
  17. 0
      amap_flutter/ios/Assets/.gitkeep
  18. 19
      amap_flutter/ios/Classes/AmapFlutterPlugin.swift
  19. 14
      amap_flutter/ios/Resources/PrivacyInfo.xcprivacy
  20. 29
      amap_flutter/ios/amap_flutter.podspec
  21. 36
      amap_flutter/lib/amap_flutter.dart
  22. 45
      amap_flutter/lib/amap_flutter_method_channel.dart
  23. 49
      amap_flutter/lib/amap_flutter_platform_interface.dart
  24. 72
      amap_flutter/pubspec.yaml
  25. 62
      android/app/build.gradle.kts
  26. 87
      android/app/proguard-rules.pro
  27. 7
      android/app/src/debug/AndroidManifest.xml
  28. 64
      android/app/src/main/AndroidManifest.xml
  29. 654
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  30. 445
      android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt
  31. 242
      android/app/src/main/kotlin/com/example/deep_voice/ClassicBluetoothHelper.kt
  32. 809
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  33. 913
      android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt
  34. 344
      android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt
  35. 37
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  36. 7
      android/app/src/profile/AndroidManifest.xml
  37. 19
      android/build.gradle.kts
  38. 25
      android/settings.gradle.kts
  39. 21
      azure/LICENSE
  40. 66
      azure/README.md
  41. 757
      azure/ios/Classes/AzureAsrHelper.swift
  42. 18
      azure/ios/Classes/AzureSpeechRecognitionPlugin.swift
  43. 427
      azure/ios/Classes/AzureTtsHelper.swift
  44. 259
      azure/ios/Classes/SwiftAzureSpeechRecognitionPlugin.swift
  45. 24
      azure/ios/azure_speech_recognition.podspec
  46. 6
      azure/lib/azure_speech_recognition.dart
  47. 23
      azure/pubspec.yaml
  48. 38
      lib/core/bindings/initial_binding.dart
  49. 91
      lib/core/translations/language/de_de.dart
  50. 73
      lib/core/translations/language/en_us.dart
  51. 93
      lib/core/translations/language/es_es.dart
  52. 99
      lib/core/translations/language/fr_fr.dart
  53. 27
      lib/core/translations/language/it_it.dart
  54. 50
      lib/core/translations/language/ja_jp.dart
  55. 48
      lib/core/translations/language/ko_kr.dart
  56. 90
      lib/core/translations/language/pt_pt.dart
  57. 86
      lib/core/translations/language/ru_ru.dart
  58. 69
      lib/core/translations/language/vi_vn.dart
  59. 56
      lib/core/translations/language/zh_cn.dart
  60. 42
      lib/core/translations/language/zh_hk.dart
  61. 89
      lib/core/utils/jieli_constants.dart
  62. 53
      lib/core/utils/logger.dart
  63. 2
      lib/core/widgets/message_bubble.dart
  64. 28
      lib/data/models/appconfig.dart
  65. 65
      lib/data/models/appconfig_model.dart
  66. 70
      lib/data/models/appconfig_model.g.dart
  67. 13
      lib/data/models/events/voice_interaction_event.dart
  68. 2
      lib/data/models/message_model.dart
  69. 6
      lib/data/services/ai_service.dart
  70. 566
      lib/data/services/ble_manager.dart
  71. 125
      lib/data/services/bluetooth_manager.dart
  72. 310
      lib/data/services/bluetooth_media_button_service.dart
  73. 105
      lib/data/services/chat_history_service.dart
  74. 243
      lib/data/services/classic_bluetooth_service.dart
  75. 8
      lib/data/services/deapsound_ai_service.dart
  76. 97
      lib/data/services/function_service.dart
  77. 6
      lib/data/services/kouzi_ai_service.dart
  78. 18
      lib/data/services/network/api.dart
  79. 2
      lib/data/services/network/nw_api.dart
  80. 301
      lib/data/services/open_ai_service_adapter.dart
  81. 3
      lib/data/services/speech_factory.dart
  82. 192
      lib/data/services/speech_impl/azure_asr_service.dart
  83. 515
      lib/data/services/speech_impl/azure_tts_service.dart
  84. 236
      lib/data/services/speech_impl/flutter_asr_service.dart
  85. 347
      lib/data/services/speech_impl/volcano_asr_service.dart
  86. 285
      lib/data/services/speech_impl/volcano_tts_service.dart
  87. 209
      lib/data/services/spotify_service.dart
  88. 15
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  89. 8
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  90. 275
      lib/data/services/voice_interaction_service.dart
  91. 6
      lib/data/services/volcano_ai_service.dart
  92. 137
      lib/data/services/volcano_translation_service.dart
  93. 9
      lib/modules/agent/bindings/agent_binding.dart
  94. 572
      lib/modules/agent/controllers/agent_controller.dart
  95. 502
      lib/modules/agent/views/agent_view.dart
  96. 879
      lib/modules/agent/views/message_bubble.dart
  97. 600
      lib/modules/chat/controllers/chat_controller.dart
  98. 32
      lib/modules/chat/models/message_model.dart
  99. 69
      lib/modules/chat/views/animated_gradient_background.dart
  100. 57
      lib/modules/chat/views/chat_music_card.dart

103
.cursorrules

@ -1,10 +1,93 @@
1. 不要主动运行程序, 如flutter run
2. 不要改动android, iOS项目的配置信息,包括SDK, NDK版本等
3. 不要使用print, 而是使用logger
4. 尽可能使用GetX提供的各种功能, 而不是使用原生的StatefulWidget
5. 不要增加重试逻辑,出现失败直接返回错误
6. 尽可能避免使用try catch
7. 尽可能避免打印日志,仅在出错时记录日志
8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化
9. 请尽可能使用中文回复问题
10. 重要:不要过度设计!!
# 代码简明规范
## Dart / Flutter
1. 风格统一
• 严格遵循 Effective Dart 格式与命名惯例。
2. 日志与调试
• 禁止 print;仅在异常场景调用 logger.e(...)。
3. 业务逻辑
• 默认使用 GetX(状态、路由、依赖注入等),避免手写 StatefulWidget。
4. 异常处理
• 不写重试逻辑,失败即返回错误。
• 谨慎使用 try-catch;优先通过类型系统与空安全防御。
5. 代码演进
• 任何改动都应 “小步、安全”,杜绝一次性大改或过度设计。
## 原生层(Android / iOS)
1. 配置不可改
• 禁止修改 SDK、NDK、JDK 与 iOS 构建配置。
2. Kotlin DSL
• 原生实现一律采用 Kotlin + 协程,遵守 Kotlin 最佳实践。
• 不写 Java 代码。
## 通用要求
1. 不自动执行
• 生成脚本/代码不得包含 flutter run 等执行指令。
2. 并发优先级
• 并发场景首选 Kotlin 协程;仅在 Flutter 端按需使用 Dart Future / Stream。
3. 语言
• 与用户交流、注释及文档一律使用中文。
宗旨:保持极简,先跑通业务,再考虑优化。
# 项目架构概述
DeepSound是一个基于Flutter和GetX构建的多平台语音应用,具有语音合成、语音识别、语音聊天等功能。
## 总体架构
- 采用GetX状态管理框架
- 遵循模块化设计,各功能模块相对独立
- 使用依赖注入实现组件解耦
- 多语言支持,使用GetX国际化方案
## 主要目录结构
- lib/core: 核心组件、工具和通用功能
- lib/data: 数据模型、服务和提供者
- lib/modules: 功能模块,按业务划分
- lib/routes: 路由定义和管理
- local_plugins: 本地插件,提供原生功能支持
# 核心模块说明
## 语音处理
- 火山语音服务: 提供语音识别和合成核心能力
- Azure语音服务: 提供备选的语音识别和合成能力
- 离线TTS支持: 在线语音合成失败时的备选方案
## 聊天功能
- 基于OpenAI的对话功能
- 支持语音输入、文字输入和语音输出
- 支持多种角色和语音类型
## 用户系统
- 支持多种登录方式:Firebase、Google、Facebook
- 用户资料管理和设置
- 主题切换和语言设置
## 蓝牙配对
- 支持蓝牙设备发现和连接
- 提供简化的配对流程
- 设备管理和状态监控
## 音频处理
- 音频录制和播放
- 支持波形显示
- 音量控制和音频质量设置
# 技术栈
- Flutter: UI框架
- GetX: 状态管理和依赖注入
- Firebase: 用户认证和后端服务
- WebSocket: 实时通信
- SQLite: 本地数据存储
- 原生插件: 语音识别、合成和蓝牙功能
# 插件说明
- jieli: 杰理蓝牙设备连接和控制插件
- volcano_speech: 火山语音服务插件,提供语音识别和合成
- azure_speech: 微软Azure语音服务插件
- open_ai_service: OpenAI服务插件,提供AI对话能力
- speech_to_text: 通用语音识别插件
# 环境配置
- 环境变量通过.env文件管理
- 支持不同环境的配置切换
- 敏感信息不应直接硬编码在代码中

2
.gitignore

@ -48,6 +48,8 @@ pubspec.lock
.pub/
/build/
.cxx/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle

10
.mcp copy.json

@ -0,0 +1,10 @@
{
"mcpServers": {
"web-search": {
"url": "http://mcp.ideapsound.com:8000/sse"
},
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=df7b0b6747c632fc38ddad22e7176646"
}
}
}

10
.mcp.json

@ -0,0 +1,10 @@
{
"mcpServers": {
"web-search": {
"url": "http://mcp.ideapsound.com:8000/sse"
},
"mp3": {
"url": "https://api.deapsound.com/sse"
}
}
}

33
amap_flutter/.gitignore

@ -0,0 +1,33 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
build/

33
amap_flutter/.metadata

@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: android
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
amap_flutter/CHANGELOG.md

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
amap_flutter/LICENSE

@ -0,0 +1 @@
TODO: Add your license here.

15
amap_flutter/README.md

@ -0,0 +1,15 @@
# amap_flutter
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

4
amap_flutter/analysis_options.yaml

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

9
amap_flutter/android/.gitignore

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

54
amap_flutter/android/build.gradle

@ -0,0 +1,54 @@
group = "com.example.amap_flutter"
version = "1.0"
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.7.0")
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: "com.android.library"
android {
namespace = "com.example.amap_flutter"
compileSdk = 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
defaultConfig {
minSdk = 21
}
dependencies {
implementation 'com.amap.api:3dmap-location-search:latest.integration'
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.0.0")
}
testOptions {
unitTests.all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}

1
amap_flutter/android/settings.gradle

@ -0,0 +1 @@
rootProject.name = 'amap_flutter'

3
amap_flutter/android/src/main/AndroidManifest.xml

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.amap_flutter">
</manifest>

169
amap_flutter/android/src/main/java/com/example/amap_flutter/AmapFlutterPlugin.java

@ -0,0 +1,169 @@
package com.example.amap_flutter;
import android.content.Context;
import androidx.annotation.NonNull;
import android.util.Log;
import com.amap.api.location.AMapLocation;
import com.amap.api.location.AMapLocationClient;
import com.amap.api.location.AMapLocationClientOption;
import com.amap.api.location.AMapLocationListener;
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 java.util.HashMap;
import java.util.Map;
/** AmapFlutterPlugin */
public class AmapFlutterPlugin implements FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel methodChannel;
private EventChannel eventChannel;
private Context applicationContext;
private AMapLocationClient mLocationClient;
private EventChannel.EventSink eventSink;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
applicationContext = flutterPluginBinding.getApplicationContext();
methodChannel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "amap_flutter");
methodChannel.setMethodCallHandler(this);
// 初始化EventChannel
eventChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "amap_flutter/events");
eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
eventSink = events;
}
@Override
public void onCancel(Object arguments) {
eventSink = null;
}
});
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "setApiKey":
setApiKey(call, result);
break;
case "initLocation":
initLocation(result);
break;
case "startLocation":
startLocation(result);
break;
case "stopLocation":
stopLocation(result);
break;
case "dispose":
dispose(result);
break;
default:
result.notImplemented();
}
}
private void setApiKey(MethodCall call, Result result) {
try {
String apiKey = call.argument("androidKey");
AMapLocationClient.updatePrivacyShow(applicationContext, true, true);
AMapLocationClient.updatePrivacyAgree(applicationContext, true);
result.success(null);
} catch (Exception e) {
result.error("INIT_ERROR", "初始化失败", e.getMessage());
}
}
private void initLocation(Result result) {
try {
mLocationClient = new AMapLocationClient(applicationContext);
// 配置定位参数
AMapLocationClientOption option = new AMapLocationClientOption();
option.setOnceLocation(true);
option.setNeedAddress(true);
mLocationClient.setLocationOption(option);
mLocationClient.setLocationListener(mLocationListener);
result.success(null);
} catch (Exception e) {
result.error("INIT_ERROR", "初始化失败", e.getMessage());
}
}
private void startLocation(Result result) {
mLocationClient.startLocation();
result.success(null);
}
private void stopLocation(Result result) {
destroyLocationClient();
result.success(null);
}
private void dispose(Result result) {
try {
destroyLocationClient();
result.success(null);
} catch (Exception e) {
result.error("DISPOSE_ERROR", "资源释放失败", e.getMessage());
}
}
private void destroyLocationClient() {
if (mLocationClient != null) {
try {
// 停止定位
mLocationClient.stopLocation();
// 销毁定位客户端
mLocationClient.onDestroy();
mLocationClient = null;
// 关闭事件通道
if (eventSink != null) {
eventSink.endOfStream();
eventSink = null;
}
} catch (Exception e) {
Log.e("AmapFlutter", "销毁定位客户端失败", e);
}
}
}
AMapLocationListener mLocationListener = new AMapLocationListener() {
@Override
public void onLocationChanged(AMapLocation amapLocation) {
if (eventSink == null) return;
if (amapLocation != null) {
if (amapLocation.getErrorCode() == 0) {
Map<String, Object> locationData = new HashMap<>();
locationData.put("latitude", amapLocation.getLatitude());
locationData.put("longitude", amapLocation.getLongitude());
locationData.put("city", amapLocation.getCity());
locationData.put("address", amapLocation.getAddress());
eventSink.success(locationData);
}
}
}
};
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
methodChannel.setMethodCallHandler(null);
eventChannel.setStreamHandler(null);
destroyLocationClient();
}
}

38
amap_flutter/ios/.gitignore

@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

0
amap_flutter/ios/Assets/.gitkeep

19
amap_flutter/ios/Classes/AmapFlutterPlugin.swift

@ -0,0 +1,19 @@
import Flutter
import UIKit
public class AmapFlutterPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "amap_flutter", binaryMessenger: registrar.messenger())
let instance = AmapFlutterPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
default:
result(FlutterMethodNotImplemented)
}
}
}

14
amap_flutter/ios/Resources/PrivacyInfo.xcprivacy

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

29
amap_flutter/ios/amap_flutter.podspec

@ -0,0 +1,29 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint amap_flutter.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'amap_flutter'
s.version = '0.0.1'
s.summary = 'A new Flutter project.'
s.description = <<-DESC
A new Flutter project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
# If your plugin requires a privacy manifest, for example if it uses any
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
# plugin's privacy impact, and then uncomment this line. For more information,
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
# s.resource_bundles = {'amap_flutter_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
end

36
amap_flutter/lib/amap_flutter.dart

@ -0,0 +1,36 @@
import 'amap_flutter_platform_interface.dart';
class AmapFlutter {
// 初始化SDK
Future<void> initSdk({String? iosKey, String? androidKey}) async {
await AmapFlutterPlatform.instance.setApiKey(
iosKey: iosKey,
androidKey: androidKey,
);
}
// 定位初始化
Future<void> initLocation() async {
return AmapFlutterPlatform.instance.initLocation();
}
// 开始定位
Future<void> startLocation() async {
return AmapFlutterPlatform.instance.startLocation();
}
// 监听定位返回位置
Stream<dynamic> locationStream() {
return AmapFlutterPlatform.instance.locationStream();
}
// 停止定位
Future<void> stopLocation() async {
await AmapFlutterPlatform.instance.stopLocation();
}
// 销毁
Future<void> dispose() async {
await AmapFlutterPlatform.instance.dispose();
}
}

45
amap_flutter/lib/amap_flutter_method_channel.dart

@ -0,0 +1,45 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'amap_flutter_platform_interface.dart';
/// An implementation of [AmapFlutterPlatform] that uses method channels.
class MethodChannelAmapFlutter extends AmapFlutterPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('amap_flutter');
final eventChannel = const EventChannel('amap_flutter/events');
@override
Future<void> setApiKey({String? iosKey, String? androidKey}) async {
await methodChannel.invokeMethod('setApiKey', {
"iosKey": iosKey,
"androidKey": androidKey,
});
}
@override
Future<void> initLocation() async {
return await methodChannel.invokeMethod('initLocation');
}
@override
Future<void> startLocation() async {
return await methodChannel.invokeMethod('startLocation');
}
@override
Stream<dynamic> locationStream() {
return eventChannel.receiveBroadcastStream();
}
@override
Future<void> stopLocation() async {
await methodChannel.invokeMethod('stopLocation');
}
@override
Future<void> dispose() async {
await methodChannel.invokeMethod('dispose');
}
}

49
amap_flutter/lib/amap_flutter_platform_interface.dart

@ -0,0 +1,49 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'amap_flutter_method_channel.dart';
abstract class AmapFlutterPlatform extends PlatformInterface {
/// Constructs a AmapFlutterPlatform.
AmapFlutterPlatform() : super(token: _token);
static final Object _token = Object();
static AmapFlutterPlatform _instance = MethodChannelAmapFlutter();
/// The default instance of [AmapFlutterPlatform] to use.
///
/// Defaults to [MethodChannelAmapFlutter].
static AmapFlutterPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [AmapFlutterPlatform] when
/// they register themselves.
static set instance(AmapFlutterPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<void> setApiKey({String? iosKey, String? androidKey}) {
throw UnimplementedError('init() has not been implemented.');
}
Future<void> initLocation() {
throw UnimplementedError('initLocation() has not been implemented.');
}
Future<void> startLocation() {
throw UnimplementedError('startLocation() has not been implemented.');
}
Stream<dynamic> locationStream() {
throw UnimplementedError('locationStream() has not been implemented.');
}
Future<void> stopLocation() {
throw UnimplementedError('stopLocation() has not been implemented.');
}
Future<void> dispose() {
throw UnimplementedError('dispose() has not been implemented.');
}
}

72
amap_flutter/pubspec.yaml

@ -0,0 +1,72 @@
name: amap_flutter
description: "A new Flutter project."
version: 0.0.1
homepage:
environment:
sdk: ^3.7.0
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.example.amap_flutter
pluginClass: AmapFlutterPlugin
ios:
pluginClass: AmapFlutterPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package

62
android/app/build.gradle.kts

@ -3,6 +3,9 @@ import java.io.FileInputStream
repositories {
google()
mavenCentral()
flatDir {
dirs(project(":spotify_sdk").projectDir.resolve("libs"))
}
}
plugins {
@ -34,12 +37,37 @@ android {
jvmTarget = JavaVersion.VERSION_11.toString()
}
// 添加lint选项
lintOptions {
isCheckReleaseBuilds = false
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
defaultConfig {
manifestPlaceholders.putAll(
mapOf(
"redirectSchemeName" to "spotify-sdk",
"redirectHostName" to "auth"
)
)
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.yunqiinnovation.deepsound"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 24 // Updated to meet azure_speech_recognition_null_safety requirements
minSdk = 26 // 更新为26以满足ble_service库的要求
targetSdk = 34 // 设置为 Android 13 (API 33),以匹配 FOREGROUND_SERVICE_MEDIA_PLAYBACK 权限的要求
versionCode = flutter.versionCode
versionName = flutter.versionName
@ -69,12 +97,22 @@ android {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
isMinifyEnabled = false
isShrinkResources = false
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
// signingConfig = signingConfigs.getByName("debug")
}
debug {
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
@ -82,14 +120,19 @@ android {
dependencies {
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
// 添加本地插件模块依赖
implementation(project(":spotify_sdk"))
implementation(project(":deepvoice_utils"))
implementation(project(":ble_service"))
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3")
// 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// Microsoft 语音识别SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.42.0")
// 添加androidx.media依赖
implementation("androidx.media:media:1.6.0")
@ -97,14 +140,7 @@ dependencies {
// 添加legacy-support-v4依赖
implementation("androidx.legacy:legacy-support-v4:1.0.0")
// 添加 AndroidX Security 加密 SharedPreferences 依赖
implementation("androidx.security:security-crypto:1.1.0-alpha06")
// // 添加火山语音合成SDK依赖
// implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8")
// 添加火山语音识别SDK依赖
// implementation("com.bytedance.speechengine:speechengine_tob:0.0.5")
}
flutter {

87
android/app/proguard-rules.pro

@ -0,0 +1,87 @@
# Flutter Framework
# 这些规则保留了 Flutter 引擎和插件系统正常运行所需的类。
-ignorewarnings
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.embedding.android.** { *; }
-keep class io.flutter.embedding.engine.** { *; }
-keep class io.flutter.embedding.engine.plugins.** { *; }
-keep class io.flutter.plugin.common.** { *; }
-keep class io.flutter.plugins.** { *; }
-keep class io.flutter.plugin.platform.** { *; }
# 保留你的应用程序的主 Activity。
# !!! 请将 com.example.deep_voice.MainActivity 替换为你的实际包名和 Activity 类名 !!!
-keep class com.yunqiinnovation.deepsound.MainActivity { *; }
# 保留 open_ai_service 包下的所有类
-keep class com.yunqiinnovation.open_ai_service.** { *; }
# 保留 MCPClient 相关类
-keep class com.yunqiinnovation.open_ai_service.MCPClient { *; }
-keep class com.yunqiinnovation.open_ai_service.MCPSubClient { *; }
-keep class com.yunqiinnovation.open_ai_service.FunctionHandler { *; }
-keep class com.yunqiinnovation.open_ai_service.ToolType { *; }
-keep class com.yunqiinnovation.open_ai_service.mcp.** { *; }
-keep class com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport { *; }
-keep class com.yunqiinnovation.open_ai_service.SystemFunctionHandler { *; }
-keep class com.yunqiinnovation.open_ai_service.OpenAIService { *; }
-keep class com.yunqiinnovation.open_ai_service.OpenAIService$* { *; }
# 保留 OpenAIServicePlugin 类
-keep class com.yunqiinnovation.open_ai_service.OpenAIServicePlugin { *; }
# 保留 Jackson 相关类
-keep class com.fasterxml.jackson.** { *; }
-keep class com.fasterxml.jackson.databind.** { *; }
-keep class com.fasterxml.jackson.core.** { *; }
# 保留 Ktor 相关类
-keep class io.ktor.** { *; }
-keep class kotlinx.coroutines.** { *; }
-keep class kotlinx.serialization.** { *; }
# 如果你的应用直接使用了 AndroidX 的某些特定类(例如,通过原生代码或插件),可能需要保留它们。
# 例如,保留所有 Parcelable 实现类,如果它们被跨进程使用或序列化。
# -keep class * implements android.os.Parcelable {
# public static final android.os.Parcelable$Creator *;
# }
# 保留 MCP 相关依赖库
-keep class io.modelcontextprotocol.** { *; }
-keep class com.anthropic.** { *; }
# 保留 OkHttp 相关类
-keep class okhttp3.** { *; }
-keep class okio.** { *; }
# 如果你使用了特定的第三方库,并且这些库需要特定的 ProGuard 规则,请在此处添加。
# 例如,OkHttp/Retrofit, Gson, Glide 等通常需要保留模型类或特定方法。
# 查看库的官方文档通常会提供所需的 ProGuard 规则。
# -keepclasseswithmembers class * {
# @android.webkit.JavascriptInterface <methods>;
# }
# -keepattributes Signature
# -keepattributes *Annotation*
# 如果你有自定义的原生插件,需要保留它们的类。
# !!! 将 com.yourcompany.yourplugin.** 替换为你的插件的实际包名 !!!
# -keep class com.yourcompany.yourplugin.** { *; }
# 保留枚举类,防止 switch 语句因混淆出错
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留 R 类及其内部类,防止资源访问出错
-keep class **.R$* { *; }
# 保留 native 方法
-keepclasseswithmembernames class * {
native <methods>;
}

7
android/app/src/debug/AndroidManifest.xml

@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

64
android/app/src/main/AndroidManifest.xml

@ -6,12 +6,16 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Android 13+ 前台服务特定类型权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<!-- Android 14+ 蓝牙设备连接前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- Android 13+ 通知权限 -->
@ -27,9 +31,31 @@
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- 添加悬浮窗权限,可能有助于解决前台服务启动问题 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 添加发送短信权限 -->
<uses-permission android:name="android.permission.SEND_SMS" />
<!-- 添加读取联系人权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- 添加拨打电话权限 -->
<uses-permission android:name="android.permission.CALL_PHONE" />
<!-- 添加查询应用包权限 -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<!--允许获取网络状态,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--允许获取wifi网络信息,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--允许获取wifi状态改变,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!--后台获取位置信息,若需后台定位则必选-->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--用于申请调用A-GPS模块,卫星定位加速-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--允许写设备缓存,用于问题排查-->
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE"/>
<application
android:label="DeepSound"
android:label="DeapSound"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
@ -38,30 +64,9 @@
android:enableOnBackInvokedCallback="true"
tools:replace="android:label">
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
<meta-data android:name="com.facebook.sdk.ClientToken" android:value="@string/facebook_client_token"/>
<!-- 添加语音交互服务 -->
<service
android:name=".VoiceInteractionService"
android:foregroundServiceType="mediaPlayback"
android:exported="true"
android:enabled="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<!-- 添加媒体按钮接收器 -->
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="true">
<intent-filter android:priority="2147483647">
<action android:name="android.intent.action.MEDIA_BUTTON" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
@ -84,12 +89,25 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="yqcxdeepsound"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="e5fdc9605eabdeb5626f18f5721f343d"/>
<service android:name="com.amap.api.location.APSService"></service>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

654
android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt

@ -1,654 +0,0 @@
package com.yunqiinnovation.deepsound
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl
import android.os.Process
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
// 移除WebRTC相关导入
class AzureAsrHelper(private val context: Context) {
private var recognizer: SpeechRecognizer? = null
private var speechConfig: SpeechConfig? = null
private val TAG = "AzureAsrHelper"
private var isContinuousRecognitionActive = false
private var currentLanguage = "zh-CN"
private var subscriptionKey = ""
private var serviceRegion = ""
private var isAutoDetectLanguage = false
private var supportedLanguages = arrayOf("zh-CN", "en-US")
// 是否使用回音消除 - 内部控制常量
private val useEchoCancellation = true
// 自定义音频处理相关
private var customAudioProcessor: CustomAudioProcessor? = null
private var pushStream: PushAudioInputStream? = null
private var audioConfig: AudioConfig? = null
// 初始化SDK并创建recognizer
fun initialize(subscriptionKey: String, serviceRegion: String,
supportedLanguages: Array<String> = arrayOf("zh-CN", "en-US")): Boolean {
try {
FileLogger.d(TAG, "初始化 Azure 语音服务")
// 检查配置是否为空
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) {
FileLogger.e(TAG, "Azure 配置信息不完整")
return false
}
// 释放之前的资源
dispose()
this.subscriptionKey = subscriptionKey
this.serviceRegion = serviceRegion
// 设置语言
if (supportedLanguages.isNotEmpty()) {
this.supportedLanguages = supportedLanguages
}
// 根据支持的语言数量决定是否启用自动语言检测
this.isAutoDetectLanguage = supportedLanguages.size >= 2
// 如果只有一种语言,设置为当前语言
if (!isAutoDetectLanguage && supportedLanguages.isNotEmpty()) {
this.currentLanguage = supportedLanguages[0]
}
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
// 设置语言配置
if (isAutoDetectLanguage) {
// 设置自动语言检测
speechConfig?.setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous")
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
}
// 创建识别器
try {
if (useEchoCancellation) {
// 如果使用回音消除,创建自定义音频输入流
setupCustomAudioProcessing()
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig)
} else {
recognizer = SpeechRecognizer(speechConfig, audioConfig)
}
} else {
// 使用默认麦克风输入
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
}
FileLogger.d(TAG, "Azure 语音服务初始化成功")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "创建识别器失败: ${e.message}")
stopCustomAudioProcessing()
return false
}
} catch (e: Exception) {
FileLogger.e(TAG, "初始化失败: ${e.message}")
return false
}
}
// 重置 recognizer
private fun resetRecognizer(): Boolean {
try {
// 释放之前的 recognizer
recognizer?.close()
recognizer = null
// 停止当前的音频处理
stopCustomAudioProcessing()
// 使用现有配置重新创建 recognizer
if (speechConfig != null) {
if (useEchoCancellation) {
// 如果使用回音消除,创建自定义音频输入流
setupCustomAudioProcessing()
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig)
} else {
recognizer = SpeechRecognizer(speechConfig, audioConfig)
}
} else {
// 使用默认麦克风输入
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
}
return true
} else {
FileLogger.e(TAG, "语音配置未初始化")
return false
}
} catch (e: Exception) {
FileLogger.e(TAG, "重置识别器失败: ${e.message}")
return false
}
}
// 开始一次性语音识别
fun recognizeOnce(callback: RecognizeCallback) {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return
}
// 重置 recognizer
if (!resetRecognizer()) {
callback.onError("重置识别器失败")
return
}
try {
// 启动音频处理
startCustomAudioProcessing()
// 执行识别
val result = recognizer?.recognizeOnceAsync()?.get()
// 停止音频处理
stopCustomAudioProcessing()
if (result != null && result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language
callback.onResult(result.text, detectedLanguage ?: "")
} else {
callback.onError("未能识别语音")
}
} catch (e: Exception) {
// 停止音频处理
stopCustomAudioProcessing()
callback.onError("识别异常: ${e.message}")
}
}
// 开始连续语音识别
fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return false
}
// 如果已经在进行连续识别,先停止
if (isContinuousRecognitionActive) {
stopContinuousRecognition(callback)
}
// 重置 recognizer
if (!resetRecognizer()) {
callback.onError("重置识别器失败")
return false
}
try {
// 启动音频处理
startCustomAudioProcessing()
// 设置识别事件处理
// 最终识别结果
recognizer?.recognized?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = if (isAutoDetectLanguage) {
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
} else {
currentLanguage
}
// FileLogger.d(TAG, "最终识别结果: ${event.result.text}")
callback.onResult(event.result.text, detectedLanguage)
}
}
)
// 识别中事件
recognizer?.recognizing?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizingSpeech) {
val detectedLanguage = if (isAutoDetectLanguage) {
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
} else {
currentLanguage
}
// FileLogger.d(TAG, "识别中结果: ${event.result.text}")
callback.onRecognizing(event.result.text, detectedLanguage)
}
}
)
// 会话事件
recognizer?.sessionStarted?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
isContinuousRecognitionActive = true
callback.onSessionStarted()
}
)
recognizer?.sessionStopped?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
isContinuousRecognitionActive = false
callback.onSessionStopped()
}
)
// 取消事件
recognizer?.canceled?.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = if (event.reason == CancellationReason.Error) event.errorDetails else ""
callback.onCanceled(event.reason.toString(), errorDetails)
isContinuousRecognitionActive = false
}
)
// 开始连续识别
recognizer?.startContinuousRecognitionAsync()?.get()
isContinuousRecognitionActive = true
return true
} catch (e: Exception) {
callback.onError("开始连续识别失败: ${e.message}")
isContinuousRecognitionActive = false
stopCustomAudioProcessing()
return false
}
}
// 停止连续语音识别
fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (!isContinuousRecognitionActive || recognizer == null) {
return true
}
try {
recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false
callback.onSessionStopped()
// 停止音频处理
stopCustomAudioProcessing()
return true
} catch (e: Exception) {
callback.onError("停止连续识别失败: ${e.message}")
isContinuousRecognitionActive = false
stopCustomAudioProcessing()
return false
}
}
// 检查连续识别是否活跃
fun isContinuousRecognitionActive(): Boolean {
return isContinuousRecognitionActive
}
// 设置自定义音频处理
private fun setupCustomAudioProcessing() {
if (!useEchoCancellation) {
return
}
try {
// 1. 创建PushAudioInputStream
pushStream = PushAudioInputStream.create()
// 2. 创建AudioConfig
audioConfig = AudioConfig.fromStreamInput(pushStream)
// 3. 创建自定义音频处理器
customAudioProcessor = CustomAudioProcessor(pushStream)
FileLogger.d(TAG, "自定义音频处理设置完成")
} catch (e: Exception) {
FileLogger.e(TAG, "设置自定义音频处理失败: ${e.message}")
releaseCustomAudioProcessing()
// 降级处理:如果自定义处理设置失败,尝试使用默认麦克风
try {
FileLogger.d(TAG, "尝试降级到默认麦克风输入")
audioConfig = AudioConfig.fromDefaultMicrophoneInput()
} catch (e2: Exception) {
FileLogger.e(TAG, "默认麦克风输入设置也失败: ${e2.message}")
audioConfig = null
}
}
}
// 启动自定义音频处理
private fun startCustomAudioProcessing() {
if (!useEchoCancellation || customAudioProcessor == null) {
return
}
try {
customAudioProcessor?.startRecording()
FileLogger.d(TAG, "自定义音频处理已启动")
} catch (e: Exception) {
FileLogger.e(TAG, "启动自定义音频处理失败: ${e.message}")
}
}
// 停止自定义音频处理
private fun stopCustomAudioProcessing() {
if (!useEchoCancellation || customAudioProcessor == null) {
return
}
try {
customAudioProcessor?.stopRecording()
FileLogger.d(TAG, "自定义音频处理已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止自定义音频处理失败: ${e.message}")
}
}
// 释放自定义音频处理资源
private fun releaseCustomAudioProcessing() {
stopCustomAudioProcessing()
try {
customAudioProcessor = null
pushStream?.close()
pushStream = null
audioConfig?.close()
audioConfig = null
FileLogger.d(TAG, "自定义音频处理资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放自定义音频处理资源时出错: ${e.message}")
}
}
// 释放所有资源
fun dispose() {
try {
// 停止和释放音频处理
releaseCustomAudioProcessing()
recognizer?.close()
recognizer = null
speechConfig?.close()
speechConfig = null
isContinuousRecognitionActive = false
FileLogger.d(TAG, "语音识别资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源出错: ${e.message}")
}
}
// 自定义音频处理器 - 使用Android原生回音消除
private inner class CustomAudioProcessor(private val pushStream: PushAudioInputStream?) {
private val SAMPLE_RATE = 16000
private val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
private val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
private val BUFFER_SIZE = SAMPLE_RATE * 2 // 简化缓冲区大小计算,更稳定
private var audioRecord: AudioRecord? = null
private var echoCanceler: AcousticEchoCanceler? = null
private val isRecording = AtomicBoolean(false)
private var recordingThread: Thread? = null
// 启动录音并处理音频数据
fun startRecording() {
if (isRecording.get() || pushStream == null) {
return
}
try {
// 使用Builder模式构建AudioFormat
val audioFormat = AudioFormat.Builder()
.setSampleRate(SAMPLE_RATE)
.setEncoding(AUDIO_FORMAT)
.setChannelMask(CHANNEL_CONFIG)
.build()
// 使用Builder模式创建AudioRecord实例
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(BUFFER_SIZE)
.build()
// 检查AudioRecord初始化状态
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
FileLogger.e(TAG, "AudioRecord初始化失败,状态: ${audioRecord?.state}")
// 尝试使用DEFAULT音频源重试一次
audioRecord?.release()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(BUFFER_SIZE)
.build()
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
FileLogger.e(TAG, "AudioRecord初始化第二次尝试也失败,放弃")
releaseAudioResources()
return
} else {
FileLogger.d(TAG, "使用默认音频源成功初始化AudioRecord")
}
}
// 启用音频效果(回音消除、噪声抑制等)
enableAudioEffects()
// 启动录音
audioRecord?.startRecording()
isRecording.set(true)
// 创建录音线程
recordingThread = Thread({
val buffer = ByteArray(BUFFER_SIZE)
while (isRecording.get()) {
try {
val readSize = audioRecord?.read(buffer, 0, BUFFER_SIZE) ?: 0
if (readSize > 0) {
try {
// 将处理后的音频数据推送到流
if (readSize == buffer.size) {
// 如果读取的大小等于buffer的大小,直接写入整个buffer
pushStream.write(buffer)
} else {
// 如果只读取了部分数据,创建新的数组只包含有效数据
val validData = buffer.copyOfRange(0, readSize)
pushStream.write(validData)
}
} catch (e: Exception) {
FileLogger.e(TAG, "写入音频数据失败: ${e.message}")
break
}
} else if (readSize == 0) {
// 读取为0,可能是临时的,等待一下继续尝试
Thread.sleep(10)
} else {
// 负值表示错误
FileLogger.e(TAG, "读取音频数据失败,错误码: $readSize")
break
}
} catch (e: Exception) {
FileLogger.e(TAG, "录音线程异常: ${e.message}")
break
}
}
}, "AudioRecordingThread")
// 设置线程优先级并启动
recordingThread?.priority = Thread.MAX_PRIORITY
recordingThread?.start()
FileLogger.d(TAG, "音频录制已启动" + (if(echoCanceler?.enabled == true) ",回音消除已启用" else ""))
} catch (e: Exception) {
FileLogger.e(TAG, "启动音频录制失败: ${e.message}")
releaseAudioResources()
}
}
// 启用音频效果(回音消除、噪声抑制等)
private fun enableAudioEffects() {
try {
val audioSessionId = audioRecord?.audioSessionId ?: -1
if (audioSessionId != -1) {
// 启用回音消除
if (AcousticEchoCanceler.isAvailable()) {
try {
echoCanceler = AcousticEchoCanceler.create(audioSessionId)
if (echoCanceler != null) {
echoCanceler?.enabled = true
FileLogger.d(TAG, "回音消除已启用,会话ID: $audioSessionId")
} else {
FileLogger.w(TAG, "回音消除器创建返回null")
}
} catch (e: Exception) {
FileLogger.e(TAG, "创建回音消除器时出错: ${e.message}")
}
} else {
FileLogger.d(TAG, "设备不支持回音消除")
}
// 以下功能暂时不启用,可根据需要取消注释
/*
// 启用噪声抑制
if (NoiseSuppressor.isAvailable()) {
try {
val ns = NoiseSuppressor.create(audioSessionId)
ns?.enabled = true
FileLogger.d(TAG, "噪声抑制已启用")
} catch (e: Exception) {
FileLogger.e(TAG, "创建噪声抑制器时出错: ${e.message}")
}
}
// 启用自动增益控制
if (AutomaticGainControl.isAvailable()) {
try {
val agc = AutomaticGainControl.create(audioSessionId)
agc?.enabled = true
FileLogger.d(TAG, "自动增益控制已启用")
} catch (e: Exception) {
FileLogger.e(TAG, "创建自动增益控制时出错: ${e.message}")
}
}
*/
} else {
FileLogger.w(TAG, "无效的音频会话ID,无法启用音频效果")
}
} catch (e: Exception) {
FileLogger.e(TAG, "启用音频效果时出错: ${e.message}")
}
}
// 停止录音
fun stopRecording() {
if (!isRecording.get()) {
return
}
isRecording.set(false)
try {
// 等待录音线程结束
recordingThread?.join(1000)
// 释放资源
releaseAudioResources()
FileLogger.d(TAG, "音频录制已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止音频录制失败: ${e.message}")
}
}
// 释放音频资源
private fun releaseAudioResources() {
try {
// 停止录音
try {
if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) {
audioRecord?.stop()
}
} catch (e: Exception) {
// 忽略可能的IllegalStateException
FileLogger.w(TAG, "停止AudioRecord时出错: ${e.message}")
}
// 释放回音消除器
try {
if (echoCanceler != null) {
echoCanceler?.enabled = false
echoCanceler?.release()
echoCanceler = null
}
} catch (e: Exception) {
FileLogger.w(TAG, "释放回音消除器时出错: ${e.message}")
} finally {
echoCanceler = null
}
// 释放音频记录器
try {
audioRecord?.release()
} catch (e: Exception) {
FileLogger.w(TAG, "释放AudioRecord时出错: ${e.message}")
} finally {
audioRecord = null
}
// 重置线程
recordingThread = null
} catch (e: Exception) {
FileLogger.e(TAG, "释放音频资源失败: ${e.message}")
}
}
}
// 一次性识别回调接口
interface RecognizeCallback {
fun onResult(result: String, detectedLanguage: String = "")
fun onError(error: String)
}
// 连续识别回调接口
interface ContinuousRecognizeCallback {
fun onResult(result: String, detectedLanguage: String = "")
fun onRecognizing(recognizing: String, detectedLanguage: String = "")
fun onSessionStarted()
fun onSessionStopped()
fun onCanceled(reason: String, errorDetails: String)
fun onError(error: String)
}
}

445
android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt

@ -1,445 +0,0 @@
package com.yunqiinnovation.deepsound
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import java.util.concurrent.Future
import java.util.concurrent.Semaphore
/**
* Microsoft Text-to-Speech Helper
*
* 该类封装了微软语音 SDK 的 TTS 功能,提供简单的接口供 Flutter 调用
* 基于微软官方文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis
*/
class AzureTtsHelper(private val context: Context) {
private val TAG = "AzureTtsHelper"
private var speechConfig: SpeechConfig? = null
private var synthesizer: SpeechSynthesizer? = null
private var isInitialized = false
private var audioManager: AudioManager? = null
// 跟踪当前是否正在播放语音
private var speaking = false
// 音频输出设备类型
enum class AudioOutputType {
SPEAKER, // 扬声器
EARPIECE, // 听筒
AUTO // 自动选择(有耳机用耳机,没有则用听筒)
}
// 当前音频输出设备类型
private var currentAudioOutputType = AudioOutputType.AUTO
// 用于异步操作的信号量
private val semaphore = Semaphore(0)
// 当前语音设置
private var currentVoiceName = "zh-CN-XiaoxiaoNeural"
private var currentSpeechRate = "0%"
private var currentPitch = "0%"
private var currentVolume = "100%"
/**
* 初始化 TTS 引擎
*
* @param subscriptionKey Azure 语音服务订阅密钥
* @param serviceRegion Azure 语音服务区域
* @param language 可选,默认语言,默认为 "zh-CN"
*/
fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean {
try {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
// 设置语音合成输出格式为高质量音频
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm)
// 设置默认语言
speechConfig?.setSpeechSynthesisLanguage(language)
// 设置默认语音
speechConfig?.setSpeechSynthesisVoiceName(currentVoiceName)
// 创建音频配置 - 使用默认扬声器
val audioConfig = AudioConfig.fromDefaultSpeakerOutput()
// 创建语音合成器
synthesizer = SpeechSynthesizer(speechConfig, audioConfig)
// 设置事件监听
synthesizer?.SynthesisCompleted?.addEventListener { _, _ ->
speaking = false
semaphore.release()
}
synthesizer?.SynthesisCanceled?.addEventListener { _, _ ->
speaking = false
semaphore.release()
}
isInitialized = true
FileLogger.d(TAG, "TTS 引擎初始化成功")
// 设置默认音频输出类型为自动
if (audioManager != null) {
setAudioOutputType(AudioOutputType.AUTO)
}
return true
} catch (e: Exception) {
FileLogger.e(TAG, "TTS 引擎初始化失败: ${e.message}, ${serviceRegion}")
e.printStackTrace()
return false
}
}
/**
* 设置音频输出设备类型
*
* @param outputType 音频输出设备类型
* @return 是否设置成功
*/
fun setAudioOutputType(outputType: AudioOutputType): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false
}
if (audioManager == null) {
FileLogger.e(TAG, "AudioManager 未初始化,无法设置音频输出设备")
return false
}
try {
currentAudioOutputType = outputType
when (outputType) {
AudioOutputType.SPEAKER -> {
// 使用扬声器
audioManager?.mode = AudioManager.MODE_NORMAL
audioManager?.isSpeakerphoneOn = true
FileLogger.d(TAG, "已设置音频输出设备为扬声器")
}
AudioOutputType.EARPIECE -> {
// 使用听筒
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager?.isSpeakerphoneOn = false
FileLogger.d(TAG, "已设置音频输出设备为听筒")
}
AudioOutputType.AUTO -> {
// 检查是否有耳机连接
if (isHeadsetConnected()) {
// 有耳机,使用耳机
audioManager?.mode = AudioManager.MODE_NORMAL
audioManager?.isSpeakerphoneOn = false
FileLogger.d(TAG, "已设置音频输出设备为耳机")
} else {
// 无耳机,使用听筒
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager?.isSpeakerphoneOn = false
FileLogger.d(TAG, "已设置音频输出设备为听筒")
}
}
}
// 重新创建合成器以应用新的音频设置
recreateSynthesizer()
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置音频输出设备失败: ${e.message}")
e.printStackTrace()
return false
}
}
/**
* 检查是否有耳机连接
*/
private fun isHeadsetConnected(): Boolean {
if (audioManager == null) return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 使用新的 API 检查音频设备
val devices = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
devices?.any { device ->
device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES ||
device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
} ?: false
} else {
// 使用旧的 API 检查耳机
audioManager?.isWiredHeadsetOn == true || audioManager?.isBluetoothA2dpOn == true
}
}
/**
* 重新创建语音合成器
*/
private fun recreateSynthesizer() {
try {
// 关闭旧的合成器
synthesizer?.close()
// 创建音频配置
val audioConfig = AudioConfig.fromDefaultSpeakerOutput()
// 创建新的合成器
synthesizer = SpeechSynthesizer(speechConfig, audioConfig)
// 重新设置事件监听
synthesizer?.SynthesisCompleted?.addEventListener { _, _ ->
speaking = false
semaphore.release()
}
synthesizer?.SynthesisCanceled?.addEventListener { _, _ ->
speaking = false
semaphore.release()
}
FileLogger.d(TAG, "已重新创建语音合成器")
} catch (e: Exception) {
FileLogger.e(TAG, "重新创建语音合成器失败: ${e.message}")
e.printStackTrace()
}
}
/**
* 设置语音
*
* @param voiceName 语音名称,例如 "zh-CN-XiaoxiaoNeural"
*/
fun setVoice(voiceName: String): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false
}
if (voiceName == currentVoiceName) {
FileLogger.d(TAG, "已设置语音: $voiceName")
return true
}
try {
currentVoiceName = voiceName
speechConfig?.setSpeechSynthesisVoiceName(voiceName)
// 重新创建合成器以应用新的语音设置
recreateSynthesizer()
FileLogger.d(TAG, "已设置语音: $voiceName")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置语音失败: ${e.message}")
e.printStackTrace()
return false
}
}
/**
* 设置语音合成参数
*
* @param rate 语速,范围 -100 到 100,默认为 0
* @param pitch 音调,范围 -100 到 100,默认为 0
* @param volume 音量,范围 0 到 100,默认为 100
*/
fun setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false
}
try {
currentSpeechRate = if (rate == 0) "0%" else if (rate < 0) "${(rate * 0.9).toInt()}%" else "$rate%"
currentPitch = if (pitch == 0) "0%" else "${(pitch * 0.5).toInt()}%"
currentVolume = "${volume.coerceIn(0, 100)}%"
FileLogger.d(TAG, "已设置语音参数: 语速=$currentSpeechRate, 音调=$currentPitch, 音量=$currentVolume")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置语音参数失败: ${e.message}")
e.printStackTrace()
return false
}
}
/**
* 合成文本为语音并播放
*
* @param text 要合成的文本
* @param callback 回调接口,用于返回结果或错误
*/
fun speakText(text: String, callback: TTSCallback) {
if (!isInitialized) {
callback.onError("TTS 引擎尚未初始化===")
return
}
try {
FileLogger.d(TAG, "开始合成文本: $text")
// 生成 SSML
val ssml = generateSsml(text)
// 使用 SSML 合成语音
speakSsml(ssml, callback)
} catch (e: Exception) {
FileLogger.e(TAG, "语音合成异常: ${e.message}")
e.printStackTrace()
callback.onError("语音合成异常: ${e.message}")
}
}
/**
* 生成 SSML 文本
*
* @param text 要转换的文本
* @return SSML 格式的文本
*/
private fun generateSsml(text: String): String {
return """
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="$currentVoiceName">
<prosody rate="$currentSpeechRate" pitch="$currentPitch" volume="$currentVolume">
$text
</prosody>
</voice>
</speak>
""".trimIndent()
}
/**
* 合成 SSML 为语音并播放
*
* @param ssml SSML 格式的文本
* @param callback 回调接口,用于返回结果或错误
*/
fun speakSsml(ssml: String, callback: TTSCallback) {
if (!isInitialized) {
callback.onError("TTS 引擎尚未初始化")
return
}
try {
FileLogger.d(TAG, "开始合成 SSML")
// 重置信号量
semaphore.drainPermits()
// 异步合成语音
val task: Future<SpeechSynthesisResult> = synthesizer!!.SpeakSsmlAsync(ssml)
// 标记为正在播放
speaking = true
// 等待合成完成
Thread {
try {
// 等待合成完成信号
semaphore.acquire()
// 获取结果
val result = task.get()
when (result.reason) {
ResultReason.SynthesizingAudioCompleted -> {
FileLogger.d(TAG, "语音合成完成,音频长度: ${result.audioLength / 10000}ms,音频数据大小: ${result.audioData?.size ?: 0} 字节")
callback.onSuccess("语音合成完成")
}
ResultReason.Canceled -> {
val cancellation = SpeechSynthesisCancellationDetails.fromResult(result)
FileLogger.e(TAG, "语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}")
callback.onError("语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}")
}
else -> {
FileLogger.e(TAG, "语音合成失败kt: ${result.reason}")
callback.onError("语音合成失败kt: ${result.reason}")
}
}
result.close()
} catch (e: Exception) {
FileLogger.e(TAG, "处理语音合成结果异常: ${e.message}")
e.printStackTrace()
callback.onError("处理语音合成结果异常: ${e.message}")
}
}.start()
} catch (e: Exception) {
FileLogger.e(TAG, "语音合成异常: ${e.message}")
e.printStackTrace()
callback.onError("语音合成异常: ${e.message}")
}
}
/**
* 停止当前语音合成
*/
fun stopSpeaking(): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "TTS 引擎尚未初始化")
return false
}
try {
synthesizer?.StopSpeakingAsync()
speaking = false
FileLogger.d(TAG, "已停止语音合成")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败: ${e.message}")
e.printStackTrace()
return false
}
}
/**
* 释放资源
*/
fun dispose() {
try {
stopSpeaking()
synthesizer?.close()
speechConfig?.close()
// 恢复音频模式
if (audioManager != null) {
audioManager?.mode = AudioManager.MODE_NORMAL
audioManager?.isSpeakerphoneOn = false
}
isInitialized = false
speaking = false
FileLogger.d(TAG, "TTS 引擎已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放 TTS 引擎失败: ${e.message}")
e.printStackTrace()
}
}
/**
* 检查当前是否正在播放语音
*
* @return 是否正在播放语音
*/
fun isSpeaking(): Boolean {
return speaking
}
/**
* TTS 回调接口
*/
interface TTSCallback {
fun onSuccess(message: String)
fun onError(error: String)
}
}

242
android/app/src/main/kotlin/com/example/deep_voice/ClassicBluetoothHelper.kt

@ -1,242 +0,0 @@
package com.yunqiinnovation.deepsound
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import android.Manifest
import androidx.core.content.ContextCompat
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* 经典蓝牙辅助类,用于获取已连接的蓝牙设备信息
*/
class ClassicBluetoothHelper(private val context: Context) {
private val TAG = "ClassicBluetoothHelper"
// 缓存蓝牙代理对象
private var a2dpProxy: BluetoothProfile? = null
private var headsetProxy: BluetoothProfile? = null
/**
* 获取已连接的A2DP设备(音频输出设备)
* @param callback 回调接口,返回设备列表或错误信息
*/
fun getConnectedA2dpDevices(callback: BluetoothDevicesCallback) {
getConnectedBluetoothDevices(BluetoothProfile.A2DP, callback)
}
/**
* 获取已连接的蓝牙耳机设备
* @param callback 回调接口,返回设备列表或错误信息
*/
fun getConnectedHeadsetDevices(callback: BluetoothDevicesCallback) {
getConnectedBluetoothDevices(BluetoothProfile.HEADSET, callback)
}
/**
* 检查是否有蓝牙连接权限
* @return 是否有权限
*/
private fun hasBluetoothConnectPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
}
/**
* 检查蓝牙是否启用
* @return 蓝牙是否启用
*/
fun isBluetoothEnabled(): Boolean {
try {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter == null) {
Log.w(TAG, "设备不支持蓝牙")
return false
}
return bluetoothAdapter.isEnabled
} catch (e: Exception) {
Log.e(TAG, "检查蓝牙状态失败: ${e.message}")
return false
}
}
/**
* 获取指定配置文件的已连接蓝牙设备
* @param profile 蓝牙配置文件类型,如 BluetoothProfile.A2DP 或 BluetoothProfile.HEADSET
* @param callback 回调接口,返回设备列表或错误信息
*/
private fun getConnectedBluetoothDevices(profile: Int, callback: BluetoothDevicesCallback) {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter == null) {
Log.e(TAG, "没有找到蓝牙适配器")
callback.onError("NO_ADAPTER", "没有找到蓝牙适配器")
return
}
// 检查蓝牙权限
if (!hasBluetoothConnectPermission()) {
Log.e(TAG, "缺少BLUETOOTH_CONNECT权限")
callback.onError("PERMISSION_DENIED", "缺少蓝牙连接权限,请在应用设置中授予该权限")
return
}
try {
// 检查是否已经有缓存的代理对象
val cachedProxy = when (profile) {
BluetoothProfile.A2DP -> a2dpProxy
BluetoothProfile.HEADSET -> headsetProxy
else -> null
}
if (cachedProxy != null) {
// 使用已缓存的代理对象
processDeviceList(cachedProxy, profile, callback)
return
}
// 使用原子布尔值确保回调只被调用一次
val callbackCalled = AtomicBoolean(false)
// 使用CountDownLatch等待服务连接
val latch = CountDownLatch(1)
// 通过 getProfileProxy 获取指定 Profile (A2DP/HEADSET) 的连接状态
bluetoothAdapter.getProfileProxy(context,
object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profileId: Int, proxy: BluetoothProfile) {
Log.d(TAG, "蓝牙配置文件服务已连接: $profileId")
if (profileId == profile) {
// 缓存代理对象
when (profileId) {
BluetoothProfile.A2DP -> a2dpProxy = proxy
BluetoothProfile.HEADSET -> headsetProxy = proxy
}
try {
processDeviceList(proxy, profileId, callback)
latch.countDown()
} catch (e: Exception) {
Log.e(TAG, "处理设备信息时出错: ${e.message}")
// 确保回调只被调用一次
if (callbackCalled.compareAndSet(false, true)) {
callback.onError("DEVICE_ERROR", "处理设备信息时出错: ${e.message}")
}
latch.countDown()
}
}
}
override fun onServiceDisconnected(profileId: Int) {
// Profile 断开连接
Log.e(TAG, "蓝牙配置文件服务断开连接: $profileId")
// 清除对应的缓存代理
when (profileId) {
BluetoothProfile.A2DP -> a2dpProxy = null
BluetoothProfile.HEADSET -> headsetProxy = null
}
// 确保回调只被调用一次,但不在这里调用错误回调
// 避免服务断开时也返回错误导致重复回调
if (callbackCalled.compareAndSet(false, true) && !latch.await(0, TimeUnit.MILLISECONDS)) {
callback.onError("SERVICE_DISCONNECTED", "蓝牙配置文件服务断开连接")
latch.countDown()
}
}
}, profile
)
// 设置超时处理
Thread {
try {
// 最多等待3秒
if (!latch.await(3, TimeUnit.SECONDS)) {
Log.e(TAG, "获取蓝牙设备超时")
if (callbackCalled.compareAndSet(false, true)) {
callback.onError("TIMEOUT", "获取蓝牙设备超时")
}
}
} catch (e: InterruptedException) {
Log.e(TAG, "等待蓝牙服务被中断: ${e.message}")
if (callbackCalled.compareAndSet(false, true)) {
callback.onError("INTERRUPTED", "等待蓝牙服务被中断: ${e.message}")
}
}
}.start()
} catch (e: Exception) {
Log.e(TAG, "获取蓝牙设备失败: ${e.message}")
callback.onError("BLUETOOTH_ERROR", "获取蓝牙设备失败: ${e.message}")
}
}
/**
* 处理设备列表
*/
private fun processDeviceList(proxy: BluetoothProfile, profileId: Int, callback: BluetoothDevicesCallback) {
// 获取已连接设备
val connectedDevices = proxy.connectedDevices
// 转换为设备信息列表
val devicesList = connectedDevices.map { device ->
mapOf(
"name" to (device.name ?: "未知设备"),
"address" to device.address
)
}
Log.d(TAG, "获取到的设备列表:${devicesList.size}个设备")
// 返回设备列表(不关闭代理)
callback.onSuccess(devicesList)
}
/**
* 释放资源
*/
fun dispose() {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: return
// 释放A2DP代理
a2dpProxy?.let {
try {
bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, it)
} catch (e: Exception) {
Log.e(TAG, "关闭A2DP代理失败: ${e.message}")
}
a2dpProxy = null
}
// 释放HEADSET代理
headsetProxy?.let {
try {
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, it)
} catch (e: Exception) {
Log.e(TAG, "关闭HEADSET代理失败: ${e.message}")
}
headsetProxy = null
}
}
/**
* 蓝牙设备回调接口
*/
interface BluetoothDevicesCallback {
fun onSuccess(devices: List<Map<String, String>>)
fun onError(code: String, message: String)
}
}

809
android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt

@ -1,809 +0,0 @@
package com.yunqiinnovation.deepsound
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.EventChannel
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity
import android.content.Context
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.os.Handler
import android.os.Looper
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.yunqiinnovation.deepsound.core.utils.FileLogger
class MainActivity: FlutterActivity() {
private val AZURE_ASR_CHANNEL = "com.deep_voice.azure_asr"
private val AZURE_ASR_EVENT_CHANNEL = "com.deep_voice.azure_asr_events"
private val AZURE_TTS_CHANNEL = "com.deep_voice.azure_tts"
private val VOICE_INTERACTION_CHANNEL = "com.deep_voice.voice_interaction"
private val VOICE_INTERACTION_EVENT_CHANNEL = "com.deep_voice.voice_interaction_events"
private val CLASSIC_BLUETOOTH_CHANNEL = "com.deep_voice.classic_bluetooth"
private val CLASSIC_BLUETOOTH_EVENT_CHANNEL = "com.deep_voice.classic_bluetooth_events"
private val TAG = "MainActivity"
private lateinit var azureAsrHelper: AzureAsrHelper
private lateinit var azureTtsHelper: AzureTtsHelper
private lateinit var classicBluetoothHelper: ClassicBluetoothHelper
private var azureAsrEventSink: EventChannel.EventSink? = null
private var voiceInteractionEventSink: EventChannel.EventSink? = null
private var bluetoothEventSink: EventChannel.EventSink? = null
// 广播接收器
private val voiceInteractionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(TAG, "收到广播: ${intent?.action}")
when (intent?.action) {
VoiceInteractionService.ACTION_RECOGNITION_STARTED -> {
val timestamp = intent.getLongExtra("timestamp", 0)
Log.d(TAG, "收到语音识别启动广播: timestamp=$timestamp")
sendVoiceInteractionEvent(mapOf(
"type" to "recognition_started",
"timestamp" to timestamp
))
}
VoiceInteractionService.ACTION_CHAT_HISTORY_UPDATED -> {
val agentId = intent.getStringExtra("agentId") ?: "default"
val userMessage = intent.getStringExtra("userMessage") ?: ""
val assistantMessage = intent.getStringExtra("assistantMessage") ?: ""
val timestamp = intent.getLongExtra("timestamp", System.currentTimeMillis())
Log.d(TAG, "收到聊天记录更新广播: agentId=$agentId, timestamp=$timestamp")
Log.d(TAG, "用户消息: ${userMessage.take(50)}...")
Log.d(TAG, "助手回复: ${assistantMessage.take(50)}...")
sendChatHistoryEvent(agentId, userMessage, assistantMessage, timestamp)
}
}
}
}
// 蓝牙状态变化广播接收器
private val bluetoothStateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
BluetoothDevice.ACTION_ACL_CONNECTED -> {
val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
}
if (device != null) {
Log.d(TAG, "蓝牙设备已连接: ${device.name ?: "未知设备"} (${device.address})")
sendBluetoothEvent(mapOf(
"type" to "deviceConnected",
"device" to mapOf(
"name" to (device.name ?: "未知设备"),
"address" to device.address
),
"timestamp" to System.currentTimeMillis()
))
}
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
}
if (device != null) {
Log.d(TAG, "蓝牙设备已断开: ${device.name ?: "未知设备"} (${device.address})")
sendBluetoothEvent(mapOf(
"type" to "deviceDisconnected",
"device" to mapOf(
"name" to (device.name ?: "未知设备"),
"address" to device.address
),
"timestamp" to System.currentTimeMillis()
))
}
}
BluetoothAdapter.ACTION_STATE_CHANGED -> {
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
when (state) {
BluetoothAdapter.STATE_ON -> {
Log.d(TAG, "蓝牙已开启")
sendBluetoothEvent(mapOf(
"type" to "bluetoothStateChanged",
"state" to "on",
"timestamp" to System.currentTimeMillis()
))
}
BluetoothAdapter.STATE_OFF -> {
Log.d(TAG, "蓝牙已关闭")
sendBluetoothEvent(mapOf(
"type" to "bluetoothStateChanged",
"state" to "off",
"timestamp" to System.currentTimeMillis()
))
}
}
}
}
}
}
// 使用伴生对象存储配置信息,使其在整个应用中可访问
companion object {
// Azure 语音服务配置
var azureSpeechKey: String = ""
var azureSpeechRegion: String = ""
var volcanoAiApiKey: String = ""
// 安全存储相关常量
private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs"
private const val KEY_AZURE_SPEECH_KEY = "azure_speech_key"
private const val KEY_AZURE_SPEECH_REGION = "azure_speech_region"
private const val KEY_VOLCANO_AI_API_KEY = "volcano_ai_api_key"
// 会话管理
private const val KEY_SESSION_ID = "session_id"
private var currentSessionId = ""
/**
* 将密钥安全地存储到 EncryptedSharedPreferences
*/
fun saveKeysToSecureStorage(context: Context) {
try {
// 创建或获取主密钥
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
// 创建 EncryptedSharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(
context,
SECURE_PREFS_FILENAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 生成新的会话ID
if (currentSessionId.isEmpty()) {
currentSessionId = java.util.UUID.randomUUID().toString()
}
// 保存密钥和会话ID
sharedPreferences.edit()
.putString(KEY_AZURE_SPEECH_KEY, azureSpeechKey)
.putString(KEY_AZURE_SPEECH_REGION, azureSpeechRegion)
.putString(KEY_VOLCANO_AI_API_KEY, volcanoAiApiKey)
.putString(KEY_SESSION_ID, currentSessionId)
.apply()
FileLogger.d("MainActivity", "密钥已安全保存到加密存储中")
} catch (e: Exception) {
FileLogger.e("MainActivity", "保存密钥到加密存储失败: ${e.message}")
e.printStackTrace()
}
}
/**
* 从 EncryptedSharedPreferences 获取密钥
*/
fun loadKeysFromSecureStorage(context: Context): Boolean {
try {
// 创建或获取主密钥
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
// 获取 EncryptedSharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(
context,
SECURE_PREFS_FILENAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 检查会话ID是否匹配
val savedSessionId = sharedPreferences.getString(KEY_SESSION_ID, "") ?: ""
// 如果会话ID为空或不匹配,说明是新会话或应用被重启
if (savedSessionId.isEmpty() || currentSessionId.isEmpty() || savedSessionId != currentSessionId) {
// 会话不匹配,但尝试加载密钥,不返回错误
FileLogger.d("MainActivity", "会话ID不匹配,但尝试加载密钥")
// 设置当前会话ID为保存的会话ID
currentSessionId = savedSessionId
}
// 加载密钥
azureSpeechKey = sharedPreferences.getString(KEY_AZURE_SPEECH_KEY, "") ?: ""
azureSpeechRegion = sharedPreferences.getString(KEY_AZURE_SPEECH_REGION, "") ?: ""
volcanoAiApiKey = sharedPreferences.getString(KEY_VOLCANO_AI_API_KEY, "") ?: ""
FileLogger.d("MainActivity", "已从加密存储加载密钥")
// 检查是否成功获取所有密钥
return azureSpeechKey.isNotEmpty() && azureSpeechRegion.isNotEmpty() && volcanoAiApiKey.isNotEmpty()
} catch (e: Exception) {
FileLogger.e("MainActivity", "从加密存储加载密钥失败: ${e.message}")
e.printStackTrace()
return false
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 初始化 FileLogger
FileLogger.init(applicationContext)
// 初始化 Azure 语音服务
azureAsrHelper = AzureAsrHelper(applicationContext)
azureTtsHelper = AzureTtsHelper(applicationContext)
classicBluetoothHelper = ClassicBluetoothHelper(applicationContext)
// 注册广播接收器
registerReceivers()
// 确保 Flutter 引擎准备好后立即设置事件通道
flutterEngine?.let { engine ->
Log.d(TAG, "Flutter 引擎已存在,立即设置事件通道")
setupEventChannels(engine)
}
}
private fun registerReceivers() {
// 注册语音交互广播接收器
val voiceInteractionFilter = IntentFilter().apply {
addAction(VoiceInteractionService.ACTION_RECOGNITION_STARTED)
addAction(VoiceInteractionService.ACTION_CHAT_HISTORY_UPDATED)
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
registerReceiver(voiceInteractionReceiver, voiceInteractionFilter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(voiceInteractionReceiver, voiceInteractionFilter)
}
// 注册蓝牙状态变化广播接收器
val bluetoothFilter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
registerReceiver(bluetoothStateReceiver, bluetoothFilter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(bluetoothStateReceiver, bluetoothFilter)
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
Log.d(TAG, "开始配置 Flutter 引擎")
// 先设置事件通道,确保在插件注册之前就已准备好
setupEventChannels(flutterEngine)
// 然后调用父类方法和注册插件
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
// 设置方法通道
setupMethodChannels(flutterEngine)
// 初始化 Azure 语音服务
azureAsrHelper = AzureAsrHelper(this)
azureTtsHelper = AzureTtsHelper(this)
Log.d(TAG, "Flutter 引擎配置完成")
}
/**
* 设置事件通道
*/
private fun setupEventChannels(flutterEngine: FlutterEngine) {
Log.d(TAG, "开始设置事件通道")
try {
// 语音交互事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "语音交互事件通道开始监听")
voiceInteractionEventSink = events
}
override fun onCancel(arguments: Any?) {
voiceInteractionEventSink = null
}
}
)
// Azure ASR 事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "ASR事件通道开始监听")
azureAsrEventSink = events
}
override fun onCancel(arguments: Any?) {
azureAsrEventSink = null
}
}
)
// 设置蓝牙事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, CLASSIC_BLUETOOTH_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "蓝牙事件通道开始监听")
bluetoothEventSink = events
}
override fun onCancel(arguments: Any?) {
Log.d(TAG, "蓝牙事件通道停止监听")
bluetoothEventSink = null
}
}
)
} catch (e: Exception) {
Log.e(TAG, "设置事件通道时发生错误: ${e.message}", e)
}
}
/**
* 设置方法通道
*/
private fun setupMethodChannels(flutterEngine: FlutterEngine) {
Log.d(TAG, "开始设置方法通道")
// 设置 Azure ASR 方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey")
val region = call.argument<String>("region")
val supportedLanguages = call.argument<List<String>>("supportedLanguages")?.toTypedArray() ?: arrayOf("zh-CN", "en-US")
if (subscriptionKey == null || region == null) {
result.error("INVALID_ARGUMENTS", "订阅密钥和区域不能为空", null)
return@setMethodCallHandler
}
try {
val success = azureAsrHelper.initialize(subscriptionKey, region, supportedLanguages)
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
}
"recognizeOnce" -> {
azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
result.success(mapOf(
"text" to text,
"detectedLanguage" to detectedLanguage
))
}
override fun onError(error: String) {
result.error("RECOGNITION_ERROR", error, null)
}
})
}
"startContinuousRecognition" -> {
// 确保事件通道已准备好
if (azureAsrEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
return@setMethodCallHandler
}
val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(mapOf(
"type" to "result",
"text" to text,
"detectedLanguage" to detectedLanguage
))
}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
sendAsrEvent(mapOf(
"type" to "recognizing",
"text" to recognizing,
"detectedLanguage" to detectedLanguage
))
}
override fun onSessionStarted() {
sendAsrEvent(mapOf("type" to "sessionStarted"))
}
override fun onSessionStopped() {
sendAsrEvent(mapOf("type" to "sessionStopped"))
}
override fun onCanceled(reason: String, errorDetails: String) {
sendAsrEvent(mapOf(
"type" to "canceled",
"reason" to reason,
"errorDetails" to errorDetails
))
}
override fun onError(error: String) {
sendAsrEvent(mapOf("type" to "error", "message" to error))
}
})
result.success(success)
}
"stopContinuousRecognition" -> {
try {
if (!azureAsrHelper.isContinuousRecognitionActive()) {
result.success(true)
return@setMethodCallHandler
}
val success = azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {}
override fun onSessionStarted() {}
override fun onSessionStopped() {}
override fun onCanceled(reason: String, errorDetails: String) {}
override fun onError(error: String) {
result.error("STOP_ERROR", error, null)
}
})
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", e.message, null)
}
}
"isContinuousRecognitionActive" -> {
result.success(azureAsrHelper.isContinuousRecognitionActive())
}
"dispose" -> {
azureAsrHelper.dispose()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
// 设置 Azure TTS 方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val language = call.argument<String>("language") ?: "zh-CN"
val success = azureTtsHelper.initialize(subscriptionKey, region, language)
result.success(success)
}
"setVoice" -> {
val voiceName = call.argument<String>("voiceName") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
result.success(azureTtsHelper.setVoice(voiceName))
}
"setSpeechParams" -> {
val rate = call.argument<Int>("rate") ?: 0
val pitch = call.argument<Int>("pitch") ?: 0
val volume = call.argument<Int>("volume") ?: 100
result.success(azureTtsHelper.setSpeechParams(rate, pitch, volume))
}
"setAudioOutputType" -> {
val outputTypeStr = call.argument<String>("outputType") ?: "speaker"
val outputType = when (outputTypeStr.lowercase()) {
"speaker" -> AzureTtsHelper.AudioOutputType.SPEAKER
"earpiece" -> AzureTtsHelper.AudioOutputType.EARPIECE
"auto" -> AzureTtsHelper.AudioOutputType.AUTO
else -> AzureTtsHelper.AudioOutputType.SPEAKER
}
result.success(azureTtsHelper.setAudioOutputType(outputType))
}
"speakText" -> {
val text = call.argument<String>("text") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "文本不能为空", null)
azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
runOnUiThread { result.success(message) }
}
override fun onError(error: String) {
runOnUiThread { result.error("SPEAK_ERROR", error, null) }
}
})
}
"speakSsml" -> {
val ssml = call.argument<String>("ssml") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "SSML不能为空", null)
azureTtsHelper.speakSsml(ssml, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
runOnUiThread { result.success(message) }
}
override fun onError(error: String) {
runOnUiThread { result.error("SPEAK_ERROR", error, null) }
}
})
}
"stopSpeaking" -> {
result.success(azureTtsHelper.stopSpeaking())
}
"isSpeaking" -> {
result.success(azureTtsHelper.isSpeaking())
}
"dispose" -> {
azureTtsHelper.dispose()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
// 设置语音交互方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"startService" -> {
startVoiceInteractionService(call, result)
}
"stopService" -> {
stopVoiceInteractionService()
result.success(true)
}
"isServiceRunning" -> {
result.success(VoiceInteractionService.isServiceRunning())
}
"pauseVoiceInteraction" -> {
pauseVoiceInteraction()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
// 设置经典蓝牙方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CLASSIC_BLUETOOTH_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getConnectedA2dpDevices" -> {
try {
classicBluetoothHelper.getConnectedA2dpDevices(object : ClassicBluetoothHelper.BluetoothDevicesCallback {
override fun onSuccess(devices: List<Map<String, String>>) {
try {
result.success(devices)
} catch (e: Exception) {
Log.e(TAG, "返回A2DP设备时发生异常: ${e.message}")
}
}
override fun onError(code: String, message: String) {
try {
result.error(code, message, null)
} catch (e: Exception) {
Log.e(TAG, "返回A2DP设备错误时发生异常: ${e.message}")
}
}
})
} catch (e: Exception) {
try {
Log.e(TAG, "获取A2DP设备时发生异常: ${e.message}")
result.error("UNEXPECTED_ERROR", "获取A2DP设备时发生异常: ${e.message}", e.toString())
} catch (e2: Exception) {
Log.e(TAG, "处理A2DP设备获取异常时发生二次异常: ${e2.message}")
}
}
}
"getConnectedHeadsetDevices" -> {
try {
classicBluetoothHelper.getConnectedHeadsetDevices(object : ClassicBluetoothHelper.BluetoothDevicesCallback {
override fun onSuccess(devices: List<Map<String, String>>) {
try {
result.success(devices)
} catch (e: Exception) {
Log.e(TAG, "返回耳机设备时发生异常: ${e.message}")
}
}
override fun onError(code: String, message: String) {
try {
result.error(code, message, null)
} catch (e: Exception) {
Log.e(TAG, "返回耳机设备错误时发生异常: ${e.message}")
}
}
})
} catch (e: Exception) {
try {
Log.e(TAG, "获取耳机设备时发生异常: ${e.message}")
result.error("UNEXPECTED_ERROR", "获取耳机设备时发生异常: ${e.message}", e.toString())
} catch (e2: Exception) {
Log.e(TAG, "处理耳机设备获取异常时发生二次异常: ${e2.message}")
}
}
}
"isBluetoothEnabled" -> {
try {
val isEnabled = classicBluetoothHelper.isBluetoothEnabled()
result.success(isEnabled)
Log.d(TAG, "检查蓝牙状态: ${if (isEnabled) "已启用" else "未启用"}")
} catch (e: Exception) {
Log.e(TAG, "检查蓝牙状态时发生异常: ${e.message}")
result.error("BLUETOOTH_ERROR", "检查蓝牙状态失败: ${e.message}", e.toString())
}
}
"initialize" -> {
result.success(true)
}
else -> result.notImplemented()
}
}
}
// ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) {
if (azureAsrEventSink == null) return
runOnUiThread {
azureAsrEventSink?.success(event)
}
}
/**
* 启动语音交互服务(带配置参数)
*/
private fun startVoiceInteractionService(call: MethodCall, result: MethodChannel.Result) {
FileLogger.d(TAG, "启动语音交互服务")
// 获取配置参数
val key = call.argument<String>("azure_speech_key") ?: ""
val region = call.argument<String>("azure_speech_region") ?: ""
val aiKey = call.argument<String>("volcano_ai_api_key") ?: ""
// 设置 Azure Speech 配置
azureSpeechKey = key
azureSpeechRegion = region
volcanoAiApiKey = aiKey
// 保存密钥到安全存储
saveKeysToSecureStorage(applicationContext)
// 创建 Intent 并启动服务
val serviceIntent = Intent(this, VoiceInteractionService::class.java)
// 在 Android O 及以上版本,需要使用 startForegroundService
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
// 返回成功结果
result.success(true)
}
/**
* 停止语音交互服务
*/
private fun stopVoiceInteractionService() {
Log.d(TAG, "停止语音交互服务")
val serviceIntent = Intent(this, VoiceInteractionService::class.java)
stopService(serviceIntent)
}
/**
* 暂停语音交互(停止TTS和语音识别,但保持服务运行)
*/
private fun pauseVoiceInteraction() {
Log.d(TAG, "发送暂停语音交互请求")
val intent = Intent(this, VoiceInteractionService::class.java).apply {
action = VoiceInteractionService.ACTION_PAUSE_VOICE_INTERACTION
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
}
/**
* 发送聊天记录事件到 Flutter
*/
private fun sendChatHistoryEvent(agentId: String, userMessage: String, assistantMessage: String, timestamp: Long) {
if (voiceInteractionEventSink == null) {
Log.e(TAG, "无法发送聊天记录事件:事件通道未准备好")
return
}
runOnUiThread {
try {
val eventData = mapOf(
"type" to "chat_history_updated",
"agentId" to agentId,
"userMessage" to userMessage,
"assistantMessage" to assistantMessage,
"timestamp" to timestamp
)
voiceInteractionEventSink?.success(eventData)
} catch (e: Exception) {
Log.e(TAG, "发送聊天记录事件到Flutter失败: ${e.message}")
}
}
}
/**
* 发送语音交互事件到 Flutter
*/
private fun sendVoiceInteractionEvent(event: Map<String, Any>) {
if (voiceInteractionEventSink == null) {
Log.e(TAG, "无法发送语音交互事件:事件通道未准备好")
return
}
runOnUiThread {
try {
voiceInteractionEventSink?.success(event)
} catch (e: Exception) {
Log.e(TAG, "发送语音交互事件到Flutter失败: ${e.message}", e)
}
}
}
// 发送蓝牙事件到Flutter
private fun sendBluetoothEvent(event: Map<String, Any>) {
if (bluetoothEventSink == null) {
Log.w(TAG, "无法发送蓝牙事件:事件通道未准备好")
return
}
try {
runOnUiThread {
try {
bluetoothEventSink?.success(event)
Log.d(TAG, "已发送蓝牙事件: ${event["type"]}")
} catch (e: Exception) {
Log.e(TAG, "发送蓝牙事件失败: ${e.message}")
}
}
} catch (e: Exception) {
Log.e(TAG, "转到UI线程发送蓝牙事件失败: ${e.message}")
}
}
override fun onDestroy() {
Log.d(TAG, "onDestroy")
// 注销所有广播接收器
try {
unregisterReceiver(voiceInteractionReceiver)
unregisterReceiver(bluetoothStateReceiver)
} catch (e: Exception) {
Log.e(TAG, "注销广播接收器失败: ${e.message}")
}
// 释放资源
azureAsrHelper.dispose()
azureTtsHelper.dispose()
classicBluetoothHelper.dispose()
super.onDestroy()
}
}

913
android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt

@ -1,913 +0,0 @@
package com.yunqiinnovation.deepsound
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.view.KeyEvent
import androidx.annotation.Nullable
import androidx.core.app.NotificationCompat
import androidx.media.session.MediaButtonReceiver
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.content.ComponentName
import android.media.AudioManager
import android.os.Handler
import android.os.Looper
import java.util.concurrent.atomic.AtomicBoolean
import org.json.JSONArray
import android.media.MediaPlayer
import android.media.AudioAttributes
import android.net.Uri
import com.yunqiinnovation.deepsound.core.utils.FileLogger
/**
* 后台语音交互 Service:
* 1) 前台服务,确保不会被系统轻易杀死
* 2) MediaSession 捕获蓝牙耳机按键
* 3) 处理录音/语音识别
*/
class VoiceInteractionService : Service() {
companion object {
private const val TAG = "VoiceInteractionService"
private const val NOTIFICATION_ID = 1001
private const val CHANNEL_ID = "voice_interaction_channel"
// 语音识别超时时间(毫秒)
private const val RECOGNITION_TIMEOUT = 8000L
// 用于跟踪服务是否正在运行
private val isRunning = AtomicBoolean(false)
// 获取服务运行状态
fun isServiceRunning(): Boolean = isRunning.get()
// 广播 Action
const val ACTION_RECOGNITION_STARTED = "com.yunqiinnovation.deepsound.ACTION_RECOGNITION_STARTED"
const val ACTION_PAUSE_VOICE_INTERACTION = "com.yunqiinnovation.deepsound.ACTION_PAUSE_VOICE_INTERACTION"
const val ACTION_CHAT_HISTORY_UPDATED = "com.yunqiinnovation.deepsound.ACTION_CHAT_HISTORY_UPDATED"
}
// 服务状态
private var isActive = false // 服务是否活跃
private var isRecognitionActive = false // 语音识别是否活跃
private var isTimeoutPaused = false // 是否因超时暂停
private var hasSpeechDetected = false // 是否检测到语音
private var isTtsSpeaking = false // 是否正在播放TTS
// 按键处理
private var lastKeyEventTime = 0L
private var keyEventCount = 0
// 活动时间
private var lastActivityTime = 0L
// 当前用户输入
private var currentUserInput = ""
// 服务组件
private lateinit var mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager
private lateinit var azureAsrHelper: AzureAsrHelper
private lateinit var azureTtsHelper: AzureTtsHelper
private lateinit var volcanoAIService: VolcanoAIService
// 定时器
private val handler = Handler(Looper.getMainLooper())
private val serviceMonitor = object : Runnable {
override fun run() {
monitorService()
handler.postDelayed(this, 1000) // 每秒执行一次
}
}
// 系统提示词
private val systemPrompt = """
你是一个智能语音助手,能够简洁明了地回答用户的问题。
请保持回答简短、准确,避免过长的解释。
如果用户的问题不清楚,请礼貌地请求澄清。
不要使用复杂的术语,除非用户明确要求。
用户用语音和你交互.
""".trimIndent()
// 添加媒体播放器
private var audioPlayer: MiniMediaPlayer? = null
override fun onCreate() {
super.onCreate()
// 初始化文件日志系统
FileLogger.init(applicationContext)
FileLogger.d(TAG, "onCreate - 语音交互服务正在创建")
// 初始化状态
isRunning.set(true)
resetState()
// 初始化组件
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
FileLogger.d(TAG, "AudioManager初始化完成")
initServices()
initMediaSession()
registerMediaButtonReceiver()
// 创建音频播放器
audioPlayer = MiniMediaPlayer(applicationContext)
audioPlayer?.play(R.raw.silence, 0f)
// 设置为媒体播放状态
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
// FileLogger.d(TAG, "设置播放状态为STATE_PAUSED")
// 启动监控和前台服务
startMonitoring()
startForegroundServiceWithNotification()
FileLogger.d(TAG, "VoiceInteractionService 初始化完成,等待蓝牙按键事件...")
}
/**
* 重置所有状态
*/
private fun resetState() {
isActive = false
isRecognitionActive = false
isTimeoutPaused = false
hasSpeechDetected = false
isTtsSpeaking = false
}
/**
* 初始化所有服务
*/
private fun initServices() {
// 创建新的Azure服务实例
FileLogger.d(TAG, "创建新的Azure服务实例")
azureAsrHelper = AzureAsrHelper(this)
azureTtsHelper = AzureTtsHelper(this)
// 尝试从静态变量获取配置
var subscriptionKey = MainActivity.azureSpeechKey
var serviceRegion = MainActivity.azureSpeechRegion
var volcanoKey = MainActivity.volcanoAiApiKey
// 如果静态变量中没有配置,尝试从加密存储中加载
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || volcanoKey.isEmpty()) {
FileLogger.d(TAG, "静态变量中的配置信息不完整,尝试从加密存储加载")
// 从加密存储加载密钥
val keysLoaded = MainActivity.loadKeysFromSecureStorage(applicationContext)
if (keysLoaded) {
// 更新本地变量
subscriptionKey = MainActivity.azureSpeechKey
serviceRegion = MainActivity.azureSpeechRegion
volcanoKey = MainActivity.volcanoAiApiKey
FileLogger.d(TAG, "已从加密存储加载配置信息")
} else {
FileLogger.e(TAG, "无法从加密存储加载配置信息")
}
}
// 初始化语音服务
if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) {
// 初始化ASR
azureAsrHelper.initialize(subscriptionKey, serviceRegion, arrayOf("zh-CN"))
// 初始化TTS
azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN")
FileLogger.d(TAG, "Azure语音服务已初始化")
} else {
FileLogger.e(TAG, "Azure配置信息不完整,无法初始化Azure服务")
}
// 初始化火山AI服务
volcanoAIService = VolcanoAIService()
// 初始化火山AI服务
if (volcanoKey.isNotEmpty()) {
volcanoAIService.initialize(volcanoKey)
FileLogger.d(TAG, "火山AI服务已初始化")
} else {
FileLogger.e(TAG, "火山AI配置信息不完整,无法初始化火山AI服务")
}
}
/**
* 初始化 MediaSession
*/
private fun initMediaSession() {
FileLogger.d(TAG, "开始初始化MediaSession")
mediaSession = MediaSessionCompat(applicationContext, "VoiceServiceSession")
// 设置标志和状态
mediaSession.setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
FileLogger.d(TAG, "MediaSession标志设置完成")
// 设置回调
mediaSession.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
} else {
@Suppress("DEPRECATION")
mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent
}
if (keyEvent != null) {
// 仅处理按下操作和相关按键
if (keyEvent.action == KeyEvent.ACTION_DOWN) {
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_HEADSETHOOK,
KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PAUSE,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
FileLogger.d(TAG, "接收到相关媒体按钮: keyCode=${keyEvent.keyCode}")
handleDoubleClick()
return true
}
}
}
FileLogger.d(TAG, "忽略不相关媒体按钮: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}")
return false
}
FileLogger.d(TAG, "收到无按键的媒体按钮事件")
return false
}
override fun onPlay() {
FileLogger.d(TAG, "收到onPlay回调")
handleDoubleClick()
return super.onPlay()
}
override fun onPause() {
FileLogger.d(TAG, "收到onPause回调")
handleDoubleClick()
return super.onPause()
}
})
// 激活 MediaSession
mediaSession.isActive = true
FileLogger.d(TAG, "MediaSession已激活")
}
/**
* 注册媒体按钮接收器
*/
private fun registerMediaButtonReceiver() {
try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.d(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
} catch (e: Exception) {
FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
}
}
/**
* 启动监控
*/
private fun startMonitoring() {
handler.post(serviceMonitor)
}
/**
* 停止监控
*/
private fun stopMonitoring() {
handler.removeCallbacks(serviceMonitor)
}
/**
* 监控服务状态
*/
private fun monitorService() {
// 确保服务保持活跃状态
if (!isActive) {
isActive = true
}
// 检查语音识别状态
if (isRecognitionActive) {
val currentTime = System.currentTimeMillis()
val elapsedTime = currentTime - lastActivityTime
// 如果超过5秒没有检测到语音,且不在TTS播放中,暂停语音识别
if (!hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT) {
FileLogger.d(TAG, "超过${RECOGNITION_TIMEOUT/1000}秒未检测到语音,停止识别")
isTimeoutPaused = true
playNotification("没有听到您说话,已暂停对话。双击耳机按钮可重新开始。")
// 停止语音识别并确保资源完全释放
stopVoiceRecognition()
// 清理识别状态
isRecognitionActive = false
hasSpeechDetected = false
}
}
}
/**
* 处理双击事件
*/
private fun handleDoubleClick() {
FileLogger.d(TAG, "处理双击事件")
//屏蔽短时间重复响应的问题
val currentTime = System.currentTimeMillis()
val timeDiff = currentTime - lastKeyEventTime
if (timeDiff < 500) {
FileLogger.d(TAG, "短时间重复响应,忽略")
return
}
lastKeyEventTime = currentTime
// 停止当前TTS播放
stopCurrentTTS()
setPlaybackState(PlaybackStateCompat.STATE_PLAYING)
// 播放提示音
playPrompt("我在!")
// audioPlayer?.play(R.raw.listening)
// 设置为播放状态
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
// 重置超时暂停标志
isTimeoutPaused = false
// 启动或重置语音识别
if (!isRecognitionActive) {
FileLogger.d(TAG, "语音识别未激活,开始启动")
// 如果之前是因为超时暂停,重新初始化语音识别组件
// if (isTimeoutPaused) {
// Log.d(TAG, "之前因超时暂停,重新初始化Azure服务")
// restartAsr()
// }
startVoiceRecognition()
} else {
FileLogger.d(TAG, "语音识别已激活,更新活动时间")
updateLastActivityTime()
hasSpeechDetected = false
}
}
/**
* 开始语音识别
*/
private fun startVoiceRecognition() {
if (isRecognitionActive) return
// 检查录音权限
if (!checkRecordAudioPermission()) {
playNotification("需要录音权限,请在设置中授予权限")
return
}
// 通知 Flutter 语音识别已启动
notifyRecognitionStarted()
isActive = true
isRecognitionActive = true
hasSpeechDetected = false
updateLastActivityTime()
try {
azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
}
}
override fun onResult(result: String, detectedLanguage: String) {
if (result.isNotEmpty()) {
processWithVolcanoAI(result)
}
// 重置状态,继续识别
hasSpeechDetected = false
updateLastActivityTime()
}
override fun onSessionStarted() {
updateLastActivityTime()
}
override fun onSessionStopped() {
isRecognitionActive = false
}
override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false
}
override fun onError(error: String) {
isRecognitionActive = false
playNotification("语音识别出错")
}
})
} catch (e: Exception) {
isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败: ${e.message}", e)
playNotification("启动语音识别失败")
}
}
/**
* 停止语音识别
*/
private fun stopVoiceRecognition() {
if (!isRecognitionActive) return
FileLogger.d(TAG, "停止语音识别")
try {
azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(result: String, detectedLanguage: String) {}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {}
override fun onSessionStarted() {}
override fun onSessionStopped() {
isRecognitionActive = false
FileLogger.d(TAG, "语音识别会话已停止")
}
override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false
FileLogger.d(TAG, "语音识别已取消: $reason")
}
override fun onError(error: String) {
isRecognitionActive = false
FileLogger.e(TAG, "停止语音识别时出错: $error")
}
})
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常: ${e.message}", e)
// 确保状态一致性
isRecognitionActive = false
}
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
isRecognitionActive = false
hasSpeechDetected = false
// 不重置 isTimeoutPaused,保留暂停原因
}
/**
* 使用VolcanoAI处理语音识别结果
*/
private fun processWithVolcanoAI(text: String) {
// 保存当前用户输入,用于后续同步聊天记录
currentUserInput = text
Thread {
try {
val messages = JSONArray().apply {
put(volcanoAIService.createUserMessage(text))
}
val response = volcanoAIService.sendMessage(messages, systemPrompt)
// 播放AI回复
speakAIResponse(response)
// 同步聊天记录到Flutter端
notifyChatHistoryUpdated("personal_assistant", text, response)
} catch (e: Exception) {
FileLogger.e(TAG, "AI处理出错: ${e.message}")
playNotification("AI处理出错")
}
}.start()
}
/**
* 播放AI回复
*/
private fun speakAIResponse(text: String) {
isTtsSpeaking = true
azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
isTtsSpeaking = false
updateLastActivityTime()
}
override fun onError(error: String) {
isTtsSpeaking = false
}
})
}
/**
* 播放提示音
*/
private fun playPrompt(message: String) {
isTtsSpeaking = true
azureTtsHelper.speakText(message, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) { isTtsSpeaking = false }
override fun onError(error: String) { isTtsSpeaking = false }
})
}
/**
* 播放通知提示音
*/
private fun playNotification(message: String) {
isTtsSpeaking = true
// 更新播放状态为播放中,增加接收蓝牙按键事件的几率
setPlaybackState(PlaybackStateCompat.STATE_PLAYING)
// 确保媒体会话处于活跃状态
mediaSession.isActive = true
azureTtsHelper.speakText(message, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
isTtsSpeaking = false
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
}
override fun onError(error: String) {
isTtsSpeaking = false
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
}
})
}
/**
* 停止当前TTS播放
*/
private fun stopCurrentTTS() {
if (isTtsSpeaking) {
azureTtsHelper.stopSpeaking()
isTtsSpeaking = false
}
}
/**
* 更新最后活动时间
*/
private fun updateLastActivityTime() {
lastActivityTime = System.currentTimeMillis()
}
/**
* 设置播放状态
*/
private fun setPlaybackState(state: Int) {
val playbackStateBuilder = PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_PLAY_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
PlaybackStateCompat.ACTION_STOP or
PlaybackStateCompat.ACTION_SEEK_TO or
PlaybackStateCompat.ACTION_FAST_FORWARD or
PlaybackStateCompat.ACTION_REWIND
)
// 使用更高的播放速度,让系统认为是真正的播放
.setState(state, 0, 1.0f, System.currentTimeMillis())
mediaSession.setPlaybackState(playbackStateBuilder.build())
// 确保MediaSession激活
if (!mediaSession.isActive) {
mediaSession.isActive = true
}
}
/**
* 检查录音权限
*/
private fun checkRecordAudioPermission(): Boolean {
val permission = android.Manifest.permission.RECORD_AUDIO
val result = applicationContext.checkCallingOrSelfPermission(permission)
return result == android.content.pm.PackageManager.PERMISSION_GRANTED
}
/**
* 启动前台服务
*/
private fun startForegroundServiceWithNotification() {
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
}
/**
* 创建通知
*/
private fun createNotification(): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Android 8.0+ 需要创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"语音交互服务",
NotificationManager.IMPORTANCE_LOW
)
manager.createNotificationChannel(channel)
}
// 点击通知打开MainActivity
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("语音助手")
.setContentText("双击蓝牙耳机按键即可开始语音交互")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.build()
}
/**
* 当 Service 被启动时调用
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
FileLogger.d(TAG, "onStartCommand: intent=$intent, action=${intent?.action}")
// 处理媒体按键 Intent
if (intent?.action == Intent.ACTION_MEDIA_BUTTON) {
FileLogger.d(TAG, "收到媒体按钮Intent: ${intent.extras}")
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent
}
if (keyEvent != null) {
FileLogger.d(TAG, "从Intent中解析到按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}")
// 使用统一的按键处理逻辑
handleKeyEvent(keyEvent)
}
} else if (intent?.action == ACTION_PAUSE_VOICE_INTERACTION) {
// 处理来自 Flutter 的暂停请求
FileLogger.d(TAG, "收到暂停请求")
pauseVoiceInteraction()
} else {
FileLogger.d(TAG, "收到其他Intent: action=${intent?.action}")
}
// 让 MediaSession 处理 Intent,MediaSession将通过callback处理事件
try {
MediaButtonReceiver.handleIntent(mediaSession, intent)
FileLogger.d(TAG, "MediaButtonReceiver处理Intent完成")
} catch (e: Exception) {
FileLogger.e(TAG, "MediaButtonReceiver处理Intent失败: ${e.message}", e)
}
return START_STICKY
}
/**
* 处理按键事件
*/
private fun handleKeyEvent(keyEvent: KeyEvent) {
FileLogger.d(TAG, "处理按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}")
// 只处理按下操作和相关媒体按键
if (keyEvent.action == KeyEvent.ACTION_DOWN) {
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PAUSE,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
FileLogger.d(TAG, "处理标准媒体按键: ${keyEvent.keyCode}")
handleDoubleClick()
}
KeyEvent.KEYCODE_HEADSETHOOK -> {
FileLogger.d(TAG, "处理HEADSETHOOK按键")
// 检测双击
val currentTime = System.currentTimeMillis()
val timeDiff = currentTime - lastKeyEventTime
FileLogger.d(TAG, "按键时间差: $timeDiff ms, 当前计数: $keyEventCount")
if (timeDiff < 500) { // 500毫秒内的按键视为连续按键
keyEventCount++
FileLogger.d(TAG, "连续按键,计数增加到: $keyEventCount")
if (keyEventCount == 1) { // 这是第二次按键,即双击
FileLogger.d(TAG, "检测到双击,执行处理")
handleDoubleClick()
keyEventCount = 0
}
} else {
FileLogger.d(TAG, "非连续按键,重置计数")
keyEventCount = 0
}
lastKeyEventTime = currentTime
}
else -> {
FileLogger.d(TAG, "忽略不相关的按键: ${keyEvent.keyCode}")
}
}
} else {
FileLogger.d(TAG, "忽略非ACTION_DOWN事件: ${keyEvent.action}")
}
}
@Nullable
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
FileLogger.d(TAG, "onDestroy - 语音交互服务正在销毁")
// 释放音频播放器
audioPlayer?.release()
audioPlayer = null
// 停止监控
stopMonitoring()
// 停止语音识别
if (isRecognitionActive) {
stopVoiceRecognition()
}
// 停止TTS
stopCurrentTTS()
// 释放 MediaSession
mediaSession.release()
// 释放Azure服务实例
FileLogger.d(TAG, "释放Azure服务实例")
azureAsrHelper.dispose()
azureTtsHelper.dispose()
// 更新服务状态
isRunning.set(false)
// 关闭日志系统
FileLogger.shutdown()
super.onDestroy()
}
/**
* 暂停语音交互(停止TTS和语音识别,但保持服务运行)
* 用于从 Flutter 端控制服务
*/
private fun pauseVoiceInteraction() {
FileLogger.d(TAG, "暂停后台语音交互(来自Flutter的请求)")
// 停止当前TTS播放
stopCurrentTTS()
// 停止语音识别
if (isRecognitionActive) {
stopVoiceRecognition()
}
// 设置为暂停状态,但保持服务活跃
isTimeoutPaused = true
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
}
/**
* 通知 Flutter 端聊天记录已更新
*/
private fun notifyChatHistoryUpdated(agentId: String, userMessage: String, assistantMessage: String) {
FileLogger.d(TAG, "通知Flutter聊天记录已更新: agentId=$agentId")
FileLogger.d(TAG, "用户消息: ${userMessage.take(50)}...")
FileLogger.d(TAG, "助手回复: ${assistantMessage.take(50)}...")
// 创建广播 Intent
val intent = Intent(ACTION_CHAT_HISTORY_UPDATED).apply {
putExtra("agentId", agentId)
putExtra("userMessage", userMessage)
putExtra("assistantMessage", assistantMessage)
putExtra("timestamp", System.currentTimeMillis())
}
// 发送广播
sendBroadcast(intent)
FileLogger.d(TAG, "已发送聊天记录广播")
}
/**
* 通知 Flutter 端语音识别已启动
*/
private fun notifyRecognitionStarted() {
FileLogger.d(TAG, "通知Flutter语音识别已启动")
// 创建广播 Intent
val intent = Intent(ACTION_RECOGNITION_STARTED).apply {
// 可以添加额外数据
putExtra("timestamp", System.currentTimeMillis())
}
// 发送广播
sendBroadcast(intent)
}
/**
* 简易媒体播放器,用于播放raw中的音频资源
*/
inner class MiniMediaPlayer(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null
/**
* 播放音频资源
* @param resId 资源ID (R.raw.xxx)
* @param volume 音量 0.0-1.0
*/
fun play(resId: Int, volume: Float = 1.0f) {
try {
// 释放之前的资源
release()
// 创建播放器并设置资源
mediaPlayer = MediaPlayer().apply {
// 设置音频资源
context.resources.openRawResourceFd(resId)?.use { fd ->
setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
}
// 设置音量
setVolume(volume.coerceIn(0f, 1f), volume.coerceIn(0f, 1f))
// 播放完成后自动释放
setOnCompletionListener { release() }
// 准备并播放
prepare()
start()
}
} catch (e: Exception) {
release()
}
}
/**
* 停止播放
*/
fun stop() {
try {
mediaPlayer?.apply {
if (isPlaying) stop()
}
} catch (e: Exception) {
// 忽略异常
}
}
/**
* 释放资源
*/
fun release() {
try {
mediaPlayer?.apply {
if (isPlaying) stop()
release()
}
mediaPlayer = null
} catch (e: Exception) {
// 忽略异常
}
}
/**
* 是否正在播放
*/
fun isPlaying() = mediaPlayer?.isPlaying == true
}
}

344
android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt

@ -1,344 +0,0 @@
package com.yunqiinnovation.deepsound
import android.util.Log
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* 火山AI服务的原生实现
*
* 参考Flutter端的VolcanoAIService实现,提供同步和异步的API调用方式
*/
class VolcanoAIService() {
private val TAG = "VolcanoAIService"
private val baseUrl = "https://ark.cn-beijing.volces.com/api/v3"
private val chatEndpoint = "/chat/completions"
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
private var apiKey: String = ""
private var isInitialized = false
/**
* 初始化火山AI服务
*
* @param apiKey 火山AI API密钥
* @return 初始化是否成功
*/
fun initialize(apiKey: String): Boolean {
this.apiKey = apiKey
isInitialized = apiKey.isNotEmpty()
if (!isInitialized) {
Log.e(TAG, "初始化失败:API key 不能为空")
} else {
Log.d(TAG, "火山AI服务初始化成功")
}
return isInitialized
}
/**
* 生成个性化问候语
*
* @param agentName 代理名称
* @param systemPrompt 系统提示词
* @param callback 回调函数,返回生成的问候语
*/
fun generateGreeting(agentName: String, systemPrompt: String, callback: (String?, Exception?) -> Unit) {
val messages = JSONArray().apply {
put(JSONObject().apply {
put("role", "system")
put("content", systemPrompt)
})
put(JSONObject().apply {
put("role", "user")
put("content", "请用一句简短的话向我打个招呼,要符合你的身份和性格特点,不要超过18个字。")
})
}
sendMessageStream(messages, systemPrompt, object : StreamCallback {
val stringBuilder = StringBuilder()
override fun onToken(token: String) {
stringBuilder.append(token)
}
override fun onComplete() {
callback(stringBuilder.toString(), null)
}
override fun onError(e: Exception) {
callback(null, e)
}
})
}
/**
* 发送消息(非流式输出)
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @return 返回AI的回复
* @throws VolcanoAIException 如果API调用失败
*/
@Throws(VolcanoAIException::class)
fun sendMessage(messages: JSONArray, systemPrompt: String): String {
// 检查是否已初始化
if (!isInitialized || apiKey.isEmpty()) {
throw VolcanoAIException("火山AI服务未初始化或API key为空,请先调用initialize方法")
}
val fullMessages = JSONArray().apply {
put(JSONObject().apply {
put("role", "system")
put("content", systemPrompt)
})
for (i in 0 until messages.length()) {
put(messages.getJSONObject(i))
}
}
val requestBody = JSONObject().apply {
put("model", "doubao-1-5-lite-32k-250115")
put("messages", fullMessages)
put("temperature", 0.7)
put("max_tokens", 2000)
put("stream", false)
}
val mediaType = "application/json".toMediaTypeOrNull()
val request = Request.Builder()
.url("$baseUrl$chatEndpoint")
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer $apiKey")
.post(requestBody.toString().toRequestBody(mediaType))
.build()
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
val errorBody = response.body?.string() ?: ""
val errorMessage = try {
JSONObject(errorBody).getJSONObject("error").getString("message")
} catch (e: Exception) {
"Unknown error occurred"
}
throw VolcanoAIException(errorMessage)
}
val responseBody = response.body?.string() ?: throw VolcanoAIException("Empty response")
val jsonResponse = JSONObject(responseBody)
if (jsonResponse.has("choices") &&
jsonResponse.getJSONArray("choices").length() > 0 &&
jsonResponse.getJSONArray("choices").getJSONObject(0).has("message")) {
return jsonResponse.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("message")
.getString("content")
}
throw VolcanoAIException("Invalid response format")
}
} catch (e: Exception) {
if (e is VolcanoAIException) throw e
throw VolcanoAIException("Failed to communicate with AI service: ${e.message}")
}
}
/**
* 发送消息(流式输出)
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @param callback 回调函数,用于接收流式输出的结果
*/
fun sendMessageStream(messages: JSONArray, systemPrompt: String, callback: StreamCallback) {
// 检查是否已初始化
if (!isInitialized || apiKey.isEmpty()) {
callback.onError(VolcanoAIException("火山AI服务未初始化或API key为空,请先调用initialize方法"))
return
}
val fullMessages = JSONArray().apply {
put(JSONObject().apply {
put("role", "system")
put("content", systemPrompt)
})
for (i in 0 until messages.length()) {
put(messages.getJSONObject(i))
}
}
val requestBody = JSONObject().apply {
put("model", "doubao-1-5-lite-32k-250115")
put("messages", fullMessages)
put("temperature", 0.7)
put("max_tokens", 2000)
put("stream", true)
}
val mediaType = "application/json".toMediaTypeOrNull()
val request = Request.Builder()
.url("$baseUrl$chatEndpoint")
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer $apiKey")
.addHeader("Accept", "text/event-stream")
.post(requestBody.toString().toRequestBody(mediaType))
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
callback.onError(VolcanoAIException("Failed to communicate with AI service: ${e.message}"))
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
val errorBody = response.body?.string() ?: ""
val errorMessage = try {
JSONObject(errorBody).getJSONObject("error").getString("message")
} catch (e: Exception) {
"Unknown error occurred"
}
callback.onError(VolcanoAIException(errorMessage))
return
}
val responseBody = response.body ?: return
val source = responseBody.source()
val bufferedSource = source.buffer
try {
while (!bufferedSource.exhausted()) {
val line = bufferedSource.readUtf8Line() ?: continue
if (line.isEmpty()) continue
if (line.startsWith("data: ")) {
val data = line.substring(6)
if (data == "[DONE]") {
callback.onComplete()
break
}
try {
val jsonData = JSONObject(data)
if (jsonData.has("choices") &&
jsonData.getJSONArray("choices").length() > 0 &&
jsonData.getJSONArray("choices").getJSONObject(0).has("delta") &&
jsonData.getJSONArray("choices").getJSONObject(0).getJSONObject("delta").has("content")) {
val content = jsonData.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("delta")
.getString("content")
callback.onToken(content)
}
} catch (e: Exception) {
// 忽略无效的JSON数据
continue
}
}
}
} catch (e: Exception) {
callback.onError(VolcanoAIException("Error processing stream: ${e.message}"))
} finally {
response.close()
}
}
})
}
/**
* 同步方式发送消息(流式输出)
*
* 注意:此方法会阻塞当前线程,请在后台线程中调用
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @return 返回完整的AI回复
* @throws VolcanoAIException 如果API调用失败
*/
@Throws(VolcanoAIException::class)
fun sendMessageStreamSync(messages: JSONArray, systemPrompt: String): String {
val result = StringBuilder()
val latch = CountDownLatch(1)
var exception: Exception? = null
sendMessageStream(messages, systemPrompt, object : StreamCallback {
override fun onToken(token: String) {
result.append(token)
}
override fun onComplete() {
latch.countDown()
}
override fun onError(e: Exception) {
exception = e
latch.countDown()
}
})
// 等待流式输出完成或出错
latch.await(60, TimeUnit.SECONDS)
if (exception != null) {
throw exception as VolcanoAIException
}
return result.toString()
}
/**
* 创建用户消息
*/
fun createUserMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "user")
put("content", content)
}
}
/**
* 创建系统消息
*/
fun createSystemMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "system")
put("content", content)
}
}
/**
* 创建助手消息
*/
fun createAssistantMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "assistant")
put("content", content)
}
}
/**
* 流式输出回调接口
*/
interface StreamCallback {
fun onToken(token: String)
fun onComplete()
fun onError(e: Exception)
}
}
/**
* 火山AI异常
*/
class VolcanoAIException(message: String) : Exception(message)

37
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -0,0 +1,37 @@
package com.yunqiinnovation.deepsound
import android.app.PendingIntent
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanSettings
import android.content.Intent
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.ble_service.BleMediaService
class MainActivity: FlutterActivity() {
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 注册标准Flutter插件
GeneratedPluginRegistrant.registerWith(flutterEngine)
}
override fun onDestroy() {
super.onDestroy()
}
}

7
android/app/src/profile/AndroidManifest.xml

@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

19
android/build.gradle.kts

@ -1,7 +1,15 @@
allprojects {
repositories {
mavenLocal()
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") }
}
}
@ -20,3 +28,14 @@ subprojects {
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10")
// 其他依赖...
}
}

25
android/settings.gradle.kts

@ -16,6 +16,7 @@ pluginManagement {
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
mavenLocal()
google()
mavenCentral()
gradlePluginPortal()
@ -24,9 +25,29 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader")
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("com.android.application") version "8.8.0" apply false
id("com.android.library") version "8.8.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
}
include(":app")
include(":azure_speech")
include(":open_ai_service")
include(":chat_storage")
include(":jieli")
include(":spotify_sdk")
include(":ble_service")
include(":deepvoice_utils")
include(":classic_bluetooth")
include(":agent_service")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
project(":chat_storage").projectDir = file("../local_plugins/chat_storage/android")
project(":jieli").projectDir = file("../local_plugins/jieli/android")
project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android")
project(":ble_service").projectDir = file("../local_plugins/ble_service/android")
project(":deepvoice_utils").projectDir = file("../local_plugins/deepvoice_utils/android")
project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android")
project(":agent_service").projectDir = file("../local_plugins/agent_service/android")

21
azure/LICENSE

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Your Company
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

66
azure/README.md

@ -1,66 +0,0 @@
# Azure Speech Recognition
A Flutter plugin for Microsoft Azure Speech services, providing both speech recognition (ASR) and text-to-speech (TTS) capabilities.
## Features
- Speech-to-text (Azure Speech Recognition)
- Text-to-speech (Azure Speech Synthesis)
- Support for multiple languages
- Language detection
- Continuous recognition
- Streaming synthesis
## Getting Started
### Prerequisites
- Azure Speech service subscription key
- Azure Speech service region
### Installation
Add this to your package's `pubspec.yaml` file:
```yaml
dependencies:
azure_speech_recognition:
path: ./azure
```
### Usage
```dart
import 'package:azure_speech_recognition/azure_speech_recognition.dart';
// Initialize the service
await AzureSpeechRecognition.initialize(
subscriptionKey: 'your_subscription_key',
region: 'your_region',
supportedLanguages: ['zh-CN', 'en-US'],
);
// Start continuous recognition
await AzureSpeechRecognition.startContinuousRecognition();
// Listen for recognition events
AzureSpeechRecognition.onRecognitionEvent.listen((event) {
if (event['type'] == 'result') {
print('Recognized: ${event['text']}');
print('Detected language: ${event['detectedLanguage']}');
}
});
// Stop recognition when done
await AzureSpeechRecognition.stopContinuousRecognition();
// Speak text
await AzureSpeechRecognition.speakText('Hello, world!');
// Clean up
await AzureSpeechRecognition.dispose();
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.

757
azure/ios/Classes/AzureAsrHelper.swift

@ -1,757 +0,0 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
import AudioToolbox
/// Azure ASR工具类,负责实现语音识别服务接口
@available(iOS 13.0, *)
class AzureAsrHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (String, [String: Any]) -> Void
/// 语音配置信息
private var speechSubscriptionKey: String = ""
private var serviceRegion: String = ""
/// 语音识别相关
private var speechConfig: SPXSpeechConfiguration?
private var recognizer: SPXSpeechRecognizer?
private var audioConfig: SPXAudioConfiguration?
private var pushStream: SPXPushAudioInputStream?
/// 音频处理相关
private var audioProcessor: CustomAudioProcessor?
private var isProcessingAudio = false
private var audioProcessingTimer: Timer?
/// 状态标志
private var isInitialized = false
private var _isContinuousRecognitionActive = false
/// 当前语言和支持的语言
private var currentLanguage = "zh-CN"
private var supportedLanguages: [String] = ["zh-CN", "en-US"]
private var isAutoDetectLanguage = false
// MARK: - 初始化
init(eventHandler: @escaping (String, [String: Any]) -> Void) {
self.eventHandler = eventHandler
super.init()
}
deinit {
dispose()
}
// MARK: - ASR Service 接口实现
/// 初始化语音识别服务
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 服务区域 (如 eastasia)
/// - supportedLanguages: 支持的语言代码数组 (可选)
/// - Returns: 初始化是否成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, supportedLanguages: [String]? = nil) -> Bool {
print("[AzureAsrHelper] 初始化 Azure 语音服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler("error", ["message": "Azure 配置信息不完整"])
return false
}
// 释放之前的资源
dispose()
// 记录配置信息
self.speechSubscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
// 设置语言
if let languages = supportedLanguages, !languages.isEmpty {
self.supportedLanguages = languages
}
// 根据支持的语言数量决定是否启用自动语言检测
isAutoDetectLanguage = self.supportedLanguages.count >= 2
// 如果只有一种语言,设置为当前语言
if !isAutoDetectLanguage && !self.supportedLanguages.isEmpty {
currentLanguage = self.supportedLanguages[0]
}
// 创建识别器和设置回调
if !createRecognizerAndSetupCallbacks() {
return false
}
print("[AzureAsrHelper] Azure 语音服务初始化成功")
isInitialized = true
return true
}
/// 创建识别器并设置回调
private func createRecognizerAndSetupCallbacks() -> Bool {
// 释放之前的 recognizer
recognizer = nil
audioConfig = nil
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置音频输入参数
try setupAudioSession()
// 创建自定义推送流,替代默认的麦克风输入
pushStream = try SPXPushAudioInputStream()
audioConfig = try SPXAudioConfiguration(streamInput: pushStream!)
// 初始化自定义音频处理器
audioProcessor = CustomAudioProcessor()
// 设置语言配置
if isAutoDetectLanguage {
// 设置自动语言检测
speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode)
// 创建自动语言检测配置
let autoDetectSourceLanguageConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
// 创建识别器
recognizer = try SPXSpeechRecognizer(
speechConfiguration: speechConfig!,
autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig,
audioConfiguration: audioConfig!
)
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
// 创建识别器
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
}
// 设置所有回调
setupAllCallbacks()
return true
} catch {
print("[AzureAsrHelper] 错误: 创建识别器失败: \(error.localizedDescription)")
eventHandler("error", ["message": "创建识别器失败: \(error.localizedDescription)"])
return false
}
}
/// 设置音频会话
private func setupAudioSession() throws {
let audioSession = AVAudioSession.sharedInstance()
// 使用playAndRecord类别允许同时录音和播放
try audioSession.setCategory(.playAndRecord,
mode: .voiceChat, // 使用voiceChat模式能够更好地支持回音消除
options: [.allowBluetooth, .defaultToSpeaker, .allowAirPlay, .mixWithOthers])
// 设置首选的输入和输出
let currentRoute = audioSession.currentRoute
// 获取当前是否连接了耳机或外部麦克风
let hasHeadphones = currentRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
}
// 如果没有耳机,明确启用内置麦克风和扬声器的回音消除
if !hasHeadphones {
try audioSession.setMode(.voiceChat) // 语音聊天模式有更强的回音消除
// 启用回音消除和噪声抑制
try audioSession.setInputGain(0.8) // 适当降低输入增益以减少扬声器音频被麦克风捕获的可能性
} else {
// 耳机模式,可以使用不同的设置
try audioSession.setMode(.voiceChat)
try audioSession.setInputGain(1.0)
}
// 设置合适的采样率
try audioSession.setPreferredSampleRate(16000.0) // Azure语音识别推荐的采样率
try audioSession.setPreferredIOBufferDuration(0.01) // 较小的缓冲区大小以减少延迟
// 激活音频会话
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
print("[AzureAsrHelper] 音频会话配置成功,已启用回音消除")
}
/// 设置所有回调
private func setupAllCallbacks() {
guard let recognizer = recognizer else { return }
// 最终识别结果
recognizer.addRecognizedEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
print("[AzureAsrHelper] 识别结果: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler("result", [
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
// 识别中事件
recognizer.addRecognizingEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizingSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
// print("[AzureAsrHelper] 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler("recognizing", [
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
// 会话事件
recognizer.addSessionStartedEventHandler { [weak self] _, _ in
guard let self = self else { return }
print("[AzureAsrHelper] 识别会话已开始")
self._isContinuousRecognitionActive = true
self.eventHandler("sessionStarted", [:])
}
recognizer.addSessionStoppedEventHandler { [weak self] _, _ in
guard let self = self else { return }
print("[AzureAsrHelper] 识别会话已结束")
self._isContinuousRecognitionActive = false
self.eventHandler("sessionStopped", [:])
}
// 取消事件
recognizer.addCanceledEventHandler { [weak self] _, event in
guard let self = self else { return }
let reason = event.reason.rawValue
let errorDetails = event.errorDetails ?? "未知错误"
print("[AzureAsrHelper] 识别取消: \(errorDetails)")
self.eventHandler("canceled", [
"reason": reason,
"errorDetails": errorDetails
])
self._isContinuousRecognitionActive = false
}
}
/// 执行一次性语音识别
/// - Returns: 是否成功启动识别
func recognizeOnce() -> Bool {
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler("error", ["message": "语音服务未初始化"])
return false
}
// 如果正在连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 确保识别器已创建
if recognizer == nil && !createRecognizerAndSetupCallbacks() {
return false
}
do {
// 启动音频处理
startAudioProcessing()
// 通知会话开始
eventHandler("sessionStarted", [:])
// 执行识别
try recognizer?.recognizeOnceAsync { [weak self] result in
guard let self = self else { return }
// 停止音频处理
self.stopAudioProcessing()
if result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: result)
self.eventHandler("result", [
"text": result.text ?? "",
"detectedLanguage": detectedLanguage
])
} else if result.reason == SPXResultReason.noMatch {
print("[AzureAsrHelper] 无匹配结果")
self.eventHandler("noMatch", [:])
} else if result.reason == SPXResultReason.canceled {
do {
let details = try SPXCancellationDetails(fromCanceledRecognitionResult: result)
let errorDetails = details.errorDetails ?? "未知错误"
self.eventHandler("error", ["message": "识别取消: \(errorDetails)"])
} catch {
print("[AzureAsrHelper] 错误: 获取取消详情失败: \(error.localizedDescription)")
self.eventHandler("error", ["message": "识别取消,无法获取详细原因"])
}
}
}
return true
} catch {
print("[AzureAsrHelper] 错误: 识别异常: \(error.localizedDescription)")
eventHandler("error", ["message": "识别异常: \(error.localizedDescription)"])
stopAudioProcessing()
return false
}
}
/// 开始连续语音识别
/// - Returns: 是否成功启动识别
func startContinuousRecognition() -> Bool {
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler("error", ["message": "语音服务未初始化"])
return false
}
// 如果已经在进行连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 确保识别器已创建
if recognizer == nil && !createRecognizerAndSetupCallbacks() {
return false
}
// 重新确保音频设置正确
do {
try setupAudioSession()
} catch {
print("[AzureAsrHelper] 警告: 设置音频会话失败: \(error.localizedDescription)")
}
do {
// 启动音频处理
startAudioProcessing()
// 启动连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
print("[AzureAsrHelper] 连续识别开始")
return true
} catch {
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
eventHandler("error", ["message": "开始连续识别失败: \(error.localizedDescription)"])
_isContinuousRecognitionActive = false
stopAudioProcessing()
return false
}
}
/// 停止连续语音识别
/// - Returns: 是否成功停止识别
func stopContinuousRecognition() -> Bool {
// 停止音频处理
stopAudioProcessing()
if !_isContinuousRecognitionActive || recognizer == nil {
return true
}
do {
try recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false
print("[AzureAsrHelper] 连续识别已停止")
return true
} catch {
print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)")
eventHandler("error", ["message": "停止连续识别失败: \(error.localizedDescription)"])
_isContinuousRecognitionActive = false
return false
}
}
/// 检查连续识别是否活跃
/// - Returns: 连续识别是否处于活跃状态
func isContinuousRecognitionActive() -> Bool {
return _isContinuousRecognitionActive
}
/// 释放资源
func dispose() {
print("[AzureAsrHelper] 释放资源")
// 停止音频处理
stopAudioProcessing()
// 停止连续识别
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 释放音频会话
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
print("[AzureAsrHelper] 警告: 释放音频会话失败: \(error.localizedDescription)")
}
// 释放资源
recognizer = nil
speechConfig = nil
audioConfig = nil
pushStream = nil
audioProcessor = nil
// 重置状态
_isContinuousRecognitionActive = false
isInitialized = false
}
/// 从结果中获取检测到的语言
private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String {
if isAutoDetectLanguage {
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result)
return langResult.language ?? currentLanguage
} catch {
print("[AzureAsrHelper] 错误: 获取检测到的语言失败: \(error.localizedDescription)")
return currentLanguage
}
} else {
return currentLanguage
}
}
// MARK: - 音频处理
/// 开始音频处理
private func startAudioProcessing() {
guard !isProcessingAudio, let audioProcessor = audioProcessor else { return }
isProcessingAudio = true
// 启动音频处理器
if !audioProcessor.startRecord() {
print("[AzureAsrHelper] 错误: 启动音频处理器失败")
eventHandler("error", ["message": "启动音频处理器失败"])
return
}
// 启动音频处理定时器
audioProcessingTimer = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] _ in
guard let self = self, self.isProcessingAudio, let processor = self.audioProcessor, let stream = self.pushStream else {
return
}
// 读取处理后的音频数据
var bytes = [UInt8](repeating: 0, count: 2560)
let bytesRead = processor.read(bytes: &bytes)
if bytesRead > 0 {
// 推送数据到Azure语音服务
let data = Data(bytes: bytes, count: bytesRead)
stream.write(data)
// 通知音频数据可用
self.eventHandler("audioData", ["data": bytes])
}
}
print("[AzureAsrHelper] 音频处理已启动")
}
/// 停止音频处理
private func stopAudioProcessing() {
// 停止定时器
audioProcessingTimer?.invalidate()
audioProcessingTimer = nil
// 停止音频处理器
audioProcessor?.stopRecord()
isProcessingAudio = false
print("[AzureAsrHelper] 音频处理已停止")
}
}
// MARK: - 自定义音频处理器
@available(iOS 13.0, *)
class CustomAudioProcessor: NSObject {
// 音频单元
private var ioUnit: AudioUnit?
// 音频格式
private var audioFormat: AudioStreamBasicDescription
// 音频缓冲
private var audioBufferList: AudioBufferList
private var audioList: [Float] = []
private let audioListQueue = DispatchQueue(label: "audioListQueue")
// 回音消除状态
private var isEchoCancellationEnabled = true
override init() {
// 设置音频格式 - 16kHz, 16位, 单声道
audioFormat = AudioStreamBasicDescription(
mSampleRate: 16000.0,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked,
mBytesPerPacket: 2,
mFramesPerPacket: 1,
mBytesPerFrame: 2,
mChannelsPerFrame: 1,
mBitsPerChannel: 16,
mReserved: 0
)
// 初始化音频缓冲
audioBufferList = AudioBufferList(
mNumberBuffers: 1,
mBuffers: AudioBuffer(
mNumberChannels: 1,
mDataByteSize: 4096,
mData: malloc(4096)
)
)
super.init()
}
deinit {
stopRecord()
free(audioBufferList.mBuffers.mData)
}
/// 启动音频处理
/// - Returns: 是否成功启动
func startRecord() -> Bool {
print("[CustomAudioProcessor] 配置音频单元")
// 创建音频组件描述 - 使用VoiceProcessingIO类型获取回音消除
var ioUnitDescription = AudioComponentDescription(
componentType: kAudioUnitType_Output,
componentSubType: kAudioUnitSubType_VoiceProcessingIO,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0
)
// 查找音频组件
guard let ioUnitRef = AudioComponentFindNext(nil, &ioUnitDescription) else {
print("[CustomAudioProcessor] 错误: 未找到音频组件")
return false
}
// 创建音频单元实例
if checkError(AudioComponentInstanceNew(ioUnitRef, &ioUnit), "创建音频单元") {
ioUnit = nil
return false
}
// 启用输入端口
var enableInput: UInt32 = 1
let kInputBus: AudioUnitElement = 1
let kOutputBus: AudioUnitElement = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input, kInputBus, &enableInput,
UInt32(MemoryLayout<UInt32>.size)), "启用输入端口") {
return false
}
// 禁用输出端口 (我们只需要输入)
var enableOutput: UInt32 = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output, kOutputBus,
&enableOutput, UInt32(MemoryLayout<UInt32>.size)), "禁用输出端口") {
return false
}
// 设置缓冲区分配标志
var flag: UInt32 = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output, kInputBus, &flag, UInt32(MemoryLayout<UInt32>.size)), "设置缓冲区分配标志") {
return false
}
// 设置音频格式
let size = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output, kInputBus, &audioFormat, size), "设置输入总线输出范围的流格式") {
return false
}
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, kOutputBus, &audioFormat, size), "设置输出总线输入范围的流格式") {
return false
}
// 启用回音消除
if isEchoCancellationEnabled {
var echoCancellation: UInt32 = 1
AudioUnitSetProperty(ioUnit!, kAUVoiceIOProperty_BypassVoiceProcessing,
kAudioUnitScope_Global, 0, &echoCancellation, UInt32(MemoryLayout<UInt32>.size))
}
// 设置输入回调 - 当有新音频数据时调用
var inputCallback = AURenderCallbackStruct(
inputProc: CustomAudioProcessor.onAudioDataAvailable,
inputProcRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
if checkError(AudioUnitSetProperty(ioUnit!,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, kInputBus,
&inputCallback, UInt32(MemoryLayout<AURenderCallbackStruct>.size)), "设置输入回调") {
return false
}
// 初始化音频单元
var hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元")
while hasError {
Thread.sleep(forTimeInterval: 0.1)
hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元")
}
// 启动音频单元
hasError = checkError(AudioOutputUnitStart(ioUnit!), "启动音频单元")
print("[CustomAudioProcessor] 音频处理器已启动,回音消除\(isEchoCancellationEnabled ? "已启用" : "已禁用")")
return !hasError
}
/// 停止音频处理
func stopRecord() {
print("[CustomAudioProcessor] 停止音频处理器")
if let ioUnit = ioUnit {
// 停止音频单元
_ = checkError(AudioOutputUnitStop(ioUnit), "停止音频单元")
// 关闭音频单元
_ = checkError(AudioUnitUninitialize(ioUnit), "反初始化音频单元")
_ = checkError(AudioComponentInstanceDispose(ioUnit), "释放音频单元")
self.ioUnit = nil
}
// 清空音频数据缓冲
audioListQueue.sync {
audioList.removeAll()
}
}
/// 音频数据回调 - 当有新的音频数据可用时调用
private static let onAudioDataAvailable: AURenderCallback = { inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData in
// 获取实例
let processor = Unmanaged<CustomAudioProcessor>.fromOpaque(inRefCon).takeUnretainedValue()
// 计算预期数据大小
let expectedDataByteSize = inNumberFrames * processor.audioFormat.mBytesPerFrame
// 确保缓冲区足够大
if processor.audioBufferList.mBuffers.mDataByteSize < expectedDataByteSize {
processor.audioBufferList.mBuffers.mData = realloc(processor.audioBufferList.mBuffers.mData, Int(expectedDataByteSize))
processor.audioBufferList.mBuffers.mDataByteSize = expectedDataByteSize
}
// 渲染音频数据
let status = processor.checkOSStatus(AudioUnitRender(processor.ioUnit!, ioActionFlags, inTimeStamp,
inBusNumber, inNumberFrames, &processor.audioBufferList),
"渲染音频数据")
// 将Int16数据转换为浮点数据进行处理
var audioDataFloat = [Float](repeating: 0.0, count: Int(inNumberFrames))
let buffer = processor.audioBufferList.mBuffers
let bufferData = buffer.mData!.assumingMemoryBound(to: Int16.self)
for j in 0..<Int(buffer.mDataByteSize / UInt32(MemoryLayout<Int16>.size)) {
// 归一化到[-1.0, 1.0]范围
audioDataFloat[j] = Float(bufferData[j]) / 32768.0
}
// 应用附加处理 (如有需要)
// processor.applyAdditionalProcessing(&audioDataFloat)
// 保存处理后的数据
if status == noErr {
processor.audioListQueue.async {
processor.audioList.append(contentsOf: audioDataFloat)
}
}
return status
}
/// 读取处理后的音频数据
/// - Parameter bytes: 输出字节数组
/// - Returns: 读取的字节数
func read(bytes: inout [UInt8]) -> Int {
return audioListQueue.sync {
// 如果没有数据,返回0
if audioList.isEmpty {
return 0
}
// 确保有足够的数据 (至少1280个样本)
if audioList.count < 1280 {
return 0
}
// 读取一帧数据 (1280个样本)
let frameLength = 1280
let buffer = Array(audioList.prefix(frameLength))
audioList.removeFirst(frameLength)
// 将浮点数据转回Int16格式
var int16Data = buffer.map { Int16($0 * 32767) }
// 转换为字节数组
let data = Data(buffer: UnsafeBufferPointer(start: &int16Data, count: int16Data.count))
bytes = [UInt8](data)
// 每个样本2字节 (16位PCM)
return frameLength * 2
}
}
/// 检查错误并打印日志
/// - Parameters:
/// - status: 操作状态
/// - operation: 操作描述
/// - Returns: 是否发生错误
private func checkError(_ status: OSStatus, _ operation: String) -> Bool {
if status != noErr {
print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)")
return true
}
return false
}
/// 检查OSStatus并返回状态
/// - Parameters:
/// - status: 操作状态
/// - operation: 操作描述
/// - Returns: 原始状态
private func checkOSStatus(_ status: OSStatus, _ operation: String) -> OSStatus {
if status != noErr {
print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)")
}
return status
}
}

18
azure/ios/Classes/AzureSpeechRecognitionPlugin.swift

@ -1,18 +0,0 @@
import Flutter
import UIKit
public class AzureSpeechRecognitionPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
if #available(iOS 13.0, *) {
SwiftAzureSpeechRecognitionPlugin.register(with: registrar)
} else {
// 如果低于iOS 13.0,返回不支持的错误
let channel = FlutterMethodChannel(name: "com.deep_voice.azure_asr", binaryMessenger: registrar.messenger())
channel.setMethodCallHandler { (call, result) in
result(FlutterError(code: "UNSUPPORTED",
message: "需要iOS 13.0及以上系统",
details: nil))
}
}
}
}

427
azure/ios/Classes/AzureTtsHelper.swift

@ -1,427 +0,0 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
/// Azure TTS工具类,负责实现TTS服务接口
@available(iOS 13.0, *)
class AzureTtsHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (String, [String: Any]) -> Void
/// 语音配置信息
private var speechSubscriptionKey: String = ""
private var serviceRegion: String = ""
/// 语音合成配置
private var speechConfig: SPXSpeechConfiguration?
/// 语音合成器
private var synthesizer: SPXSpeechSynthesizer?
/// 是否初始化成功
private var isInitialized = false
/// 当前是否正在播放
private var _isSpeaking = false
/// 音频会话配置
private var isAudioSessionConfigured = false
// MARK: - 语音设置
/// 当前语音
private var currentVoice = "zh-CN-XiaoxiaoNeural"
/// 支持的语音映射
private var voiceMap: [String: String] = [
"zh-CN": "zh-CN-XiaoxiaoNeural",
"en-US": "en-US-JennyNeural",
"ja-JP": "ja-JP-NanamiNeural",
"ko-KR": "ko-KR-SunHiNeural",
"zh-TW": "zh-TW-HsiaoChenNeural",
"zh-HK": "zh-HK-HiuMaanNeural"
]
/// 当前语音合成参数
private var currentSpeechRate = "0%"
private var currentPitch = "0%"
private var currentVolume = "100%"
// MARK: - 初始化
init(eventHandler: @escaping (String, [String: Any]) -> Void) {
self.eventHandler = eventHandler
super.init()
}
deinit {
dispose()
}
// MARK: - TTS 接口实现
/// 初始化语音合成服务
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 服务区域 (如 eastasia)
/// - language: 语言代码 (默认 zh-CN)
/// - Returns: 初始化是否成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, language: String = "zh-CN") -> Bool {
print("[AzureTtsHelper] 初始化语音合成服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler("error", ["error": "Azure 配置信息不完整"])
return false
}
// 释放之前的资源
dispose()
// 记录配置信息
self.speechSubscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
// 配置音频会话
if !configureAudioSession() {
print("[AzureTtsHelper] 警告: 音频会话配置失败,将尝试继续初始化")
}
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置默认语音
let defaultVoice = getDefaultVoiceForLanguage(language)
currentVoice = defaultVoice
speechConfig?.speechSynthesisVoiceName = defaultVoice
// 创建语音合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig!)
// 设置事件处理器
setupSynthesizerEvents()
isInitialized = true
print("[AzureTtsHelper] TTS 引擎初始化成功")
return true
} catch {
print("[AzureTtsHelper] 错误: 初始化语音合成服务失败: \(error.localizedDescription)")
eventHandler("error", ["error": "初始化语音合成服务失败: \(error.localizedDescription)"])
return false
}
}
/// 配置音频会话
private func configureAudioSession() -> Bool {
let audioSession = AVAudioSession.sharedInstance()
do {
// 使用playback类别,但支持混合和空中播放
try audioSession.setCategory(.playback,
mode: .spokenAudio,
options: [.mixWithOthers, .allowAirPlay, .duckOthers])
// 根据设备类型选择最佳配置
let currentRoute = audioSession.currentRoute
let hasHeadphones = currentRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
}
// 优化音频路由
if hasHeadphones {
// 耳机模式,使用默认设置
try audioSession.setPreferredIOBufferDuration(0.005) // 较小的缓冲区大小以减少延迟
} else {
// 扬声器模式
try audioSession.setPreferredIOBufferDuration(0.005)
}
// 避免完全激活音频会话,因为ASR可能已经激活
// 这里使用setActive(false)是为了不与ASR冲突
if !audioSession.isOtherAudioPlaying {
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
}
isAudioSessionConfigured = true
print("[AzureTtsHelper] 音频会话配置成功")
return true
} catch {
print("[AzureTtsHelper] 警告: 音频会话配置失败: \(error.localizedDescription)")
isAudioSessionConfigured = false
return false
}
}
/// 设置语音
/// - Parameter voiceName: 语音名称 (如 "zh-CN-XiaoxiaoNeural")
/// - Returns: 设置是否成功
func setVoice(voiceName: String) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
if voiceName.isEmpty {
print("[AzureTtsHelper] 错误: 声音名称为空")
eventHandler("error", ["error": "声音名称不能为空"])
return false
}
if voiceName == currentVoice {
print("[AzureTtsHelper] 已设置语音: \(voiceName)")
return true
}
print("[AzureTtsHelper] 设置声音: \(voiceName)")
currentVoice = voiceName
// 更新语音配置
if let speechConfig = speechConfig {
speechConfig.speechSynthesisVoiceName = voiceName
return true
}
return false
}
/// 设置语音合成参数
/// - Parameters:
/// - rate: 语速,范围 -100 到 100,默认为 0
/// - pitch: 音调,范围 -100 到 100,默认为 0
/// - volume: 音量,范围 0 到 100,默认为 100
/// - Returns: 是否设置成功
func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
// 转换参数格式
currentSpeechRate = formatRateParam(rate)
currentPitch = formatPitchParam(pitch)
currentVolume = formatVolumeParam(volume)
print("[AzureTtsHelper] 已设置语音参数: 语速=\(currentSpeechRate), 音调=\(currentPitch), 音量=\(currentVolume)")
return true
}
/// 合成文本为语音并播放
/// - Parameter text: 要合成的文本
/// - Returns: 操作是否成功启动
func speakText(text: String) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
if text.isEmpty {
print("[AzureTtsHelper] 警告: 要播放的文本为空")
return true
}
// 确保音频会话已配置
if !isAudioSessionConfigured {
_ = configureAudioSession()
}
print("[AzureTtsHelper] 开始语音合成: \(text.prefix(50))...")
// 生成SSML
let ssml = generateSsml(text: text)
// 直接进行SSML合成
return speakSsmlInternal(text: ssml)
}
/// 内部SSML合成和播放
private func speakSsmlInternal(text: String) -> Bool {
guard let synthesizer = synthesizer else {
print("[AzureTtsHelper] 错误: 合成器未初始化")
eventHandler("error", ["error": "合成器未初始化"])
return false
}
_isSpeaking = true
eventHandler("started", [:])
Task {
do {
// 使用异步方法进行合成并直接播放
_ = try await synthesizer.startSpeakingSsml(text)
} catch {
print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成失败: \(error.localizedDescription)"])
}
}
}
return true
}
/// 停止当前语音合成
/// - Returns: 操作是否成功
func stopSpeaking() -> Bool {
if !isInitialized || !_isSpeaking {
return true
}
// 停止合成
do {
try synthesizer?.stopSpeaking()
_isSpeaking = false
eventHandler("canceled", [:])
print("[AzureTtsHelper] 已停止语音合成")
return true
} catch {
print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)")
eventHandler("error", ["error": "停止语音合成失败: \(error.localizedDescription)"])
return false
}
}
/// 检查是否正在播放
/// - Returns: 当前是否正在播放语音
func isSpeaking() -> Bool {
return _isSpeaking
}
/// 释放资源
func dispose() {
try? stopSpeaking()
// 释放合成器和配置
synthesizer = nil
speechConfig = nil
isInitialized = false
_isSpeaking = false
isAudioSessionConfigured = false
print("[AzureTtsHelper] TTS 引擎已释放")
}
// MARK: - 私有辅助方法
/// 设置合成器事件处理
private func setupSynthesizerEvents() {
guard let synthesizer = synthesizer else { return }
// 添加书签到达事件处理
synthesizer.addBookmarkReachedEventHandler { _, e in
print("[AzureTtsHelper] 书签事件: 音频偏移: \((e.audioOffset + 5000) / 10000)ms, 文本: \"\(e.text)\"")
}
// 合成完成事件
synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in
guard let self = self else { return }
print("[AzureTtsHelper] 语音合成完成: 音频持续时间: \(e.result.audioDuration)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("completed", [:])
}
}
// 合成取消事件
synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in
guard let self = self else { return }
let result = e.result
do {
let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: result)
print("[AzureTtsHelper] 语音合成取消: 原因: \(cancellationDetails.reason)")
if cancellationDetails.reason == SPXCancellationReason.error {
print("[AzureTtsHelper] 错误代码: \(cancellationDetails.errorCode)")
print("[AzureTtsHelper] 错误详情: \(cancellationDetails.errorDetails ?? "未知")")
}
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"])
}
} catch {
print("[AzureTtsHelper] 获取取消详情时出错: \(error)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成被取消"])
}
}
}
// 合成开始事件
synthesizer.addSynthesisStartedEventHandler { _, _ in
// print("[AzureTtsHelper] 语音合成开始")
}
// 合成中事件
synthesizer.addSynthesizingEventHandler { _, _ in
// print("[AzureTtsHelper] 语音合成中")
}
}
/// 生成 SSML 文本
private func generateSsml(text: String) -> String {
return """
<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='zh-CN'>
<voice name='\(currentVoice)'>
<prosody rate='\(currentSpeechRate)' pitch='\(currentPitch)' volume='\(currentVolume)'>
\(text)
</prosody>
</voice>
</speak>
"""
}
/// 格式化语速参数
private func formatRateParam(_ rate: Int) -> String {
let clampedRate = rate.clamp(min: -100, max: 100)
if clampedRate == 0 {
return "0%"
} else if clampedRate < 0 {
return "\(Int(Double(clampedRate) * 0.9))%"
} else {
return "+\(clampedRate)%"
}
}
/// 格式化音调参数
private func formatPitchParam(_ pitch: Int) -> String {
let clampedPitch = pitch.clamp(min: -100, max: 100)
if clampedPitch == 0 {
return "0%"
} else {
return "\(Int(Double(clampedPitch) * 0.5))%"
}
}
/// 格式化音量参数
private func formatVolumeParam(_ volume: Int) -> String {
let clampedVolume = volume.clamp(min: 0, max: 100)
return "\(clampedVolume)%"
}
/// 获取指定语言的默认语音
private func getDefaultVoiceForLanguage(_ language: String) -> String {
return voiceMap[language] ?? "zh-CN-XiaoxiaoNeural"
}
}
// MARK: - 扩展
extension Int {
func clamp(min: Int, max: Int) -> Int {
if self < min { return min }
if self > max { return max }
return self
}
}

259
azure/ios/Classes/SwiftAzureSpeechRecognitionPlugin.swift

@ -1,259 +0,0 @@
import Flutter
import UIKit
import MicrosoftCognitiveServicesSpeech
import AVFoundation
@available(iOS 13.0, *)
public class SwiftAzureSpeechRecognitionPlugin: NSObject, FlutterPlugin {
private var azureChannel: FlutterMethodChannel
private var ttsChannel: FlutterMethodChannel
private var asrHelper: AzureAsrHelper
private var ttsHelper: AzureTtsHelper
private static var eventStreamHandler: AzureEventStreamHandler?
// 创建方法到通道的映射
private static var ttsMethodHandlers = [String: FlutterMethodCallHandler]()
private static var asrMethodHandlers = [String: FlutterMethodCallHandler]()
public static func register(with registrar: FlutterPluginRegistrar) {
// ASR通道
let channel = FlutterMethodChannel(name: "com.deep_voice.azure_asr", binaryMessenger: registrar.messenger())
// TTS通道
let ttsChannel = FlutterMethodChannel(name: "com.deep_voice.azure_tts", binaryMessenger: registrar.messenger())
// 设置ASR事件通道
let eventChannel = FlutterEventChannel(name: "com.deep_voice.azure_asr_events", binaryMessenger: registrar.messenger())
eventStreamHandler = AzureEventStreamHandler()
eventChannel.setStreamHandler(eventStreamHandler)
let instance = SwiftAzureSpeechRecognitionPlugin(
azureChannel: channel,
ttsChannel: ttsChannel,
eventStreamHandler: eventStreamHandler!
)
// 直接设置各自通道的处理器
channel.setMethodCallHandler(instance.handleAsrMethodCalls)
ttsChannel.setMethodCallHandler(instance.handleTtsMethodCalls)
}
// 新增直接处理方法调用的函数
private func handleTtsMethodCalls(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
handleTtsMethod(call, result)
}
private func handleAsrMethodCalls(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
handleAsrMethod(call, result)
}
init(azureChannel: FlutterMethodChannel, ttsChannel: FlutterMethodChannel, eventStreamHandler: AzureEventStreamHandler) {
self.azureChannel = azureChannel
self.ttsChannel = ttsChannel
// 创建辅助类实例,使用自定义事件回调处理器
let eventHandler: (String, [String: Any]) -> Void = { eventName, arguments in
DispatchQueue.main.async {
if let eventSink = SwiftAzureSpeechRecognitionPlugin.eventStreamHandler?.eventSink {
var eventData = arguments
eventData["type"] = eventName
eventSink(eventData)
}
}
}
asrHelper = AzureAsrHelper(eventHandler: eventHandler)
ttsHelper = AzureTtsHelper(eventHandler: eventHandler)
super.init()
}
private func handleAsrMethod(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let args = call.arguments as? Dictionary<String, Any>
switch call.method {
case "initialize":
// 仅在初始化时读取必要参数
guard let speechSubscriptionKey = args?["subscriptionKey"] as? String, !speechSubscriptionKey.isEmpty else {
let errorMsg = "语音订阅密钥不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SUBSCRIPTION_KEY", message: errorMsg, details: nil))
return
}
guard let serviceRegion = args?["region"] as? String, !serviceRegion.isEmpty else {
let errorMsg = "服务区域不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_REGION", message: errorMsg, details: nil))
return
}
let supportedLanguages = args?["supportedLanguages"] as? [String] ?? []
let success = asrHelper.initialize(
speechSubscriptionKey: speechSubscriptionKey,
serviceRegion: serviceRegion,
supportedLanguages: supportedLanguages.isEmpty ? nil : supportedLanguages
)
result(success)
case "startContinuousRecognition":
// 只有使用参数时才验证
let success = asrHelper.startContinuousRecognition()
result(success)
case "stopContinuousRecognition":
// 不需要额外参数
let success = asrHelper.stopContinuousRecognition()
result(success)
case "recognizeOnce":
// 只有使用参数时才验证
let success = asrHelper.recognizeOnce()
result(success)
case "isContinuousRecognitionActive":
// 不需要额外参数
result(asrHelper.isContinuousRecognitionActive())
case "dispose":
// 不需要额外参数
print("[AzurePlugin] 释放ASR资源")
asrHelper.dispose()
result(true)
default:
print("[AzurePlugin] 错误: 未知ASR方法: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
private func handleTtsMethod(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let args = call.arguments as? Dictionary<String, Any>
switch call.method {
case "initialize":
// 仅在初始化时验证参数
guard let speechSubscriptionKey = args?["subscriptionKey"] as? String, !speechSubscriptionKey.isEmpty else {
let errorMsg = "语音订阅密钥不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SUBSCRIPTION_KEY", message: errorMsg, details: nil))
return
}
guard let serviceRegion = args?["region"] as? String, !serviceRegion.isEmpty else {
let errorMsg = "服务区域不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_REGION", message: errorMsg, details: nil))
return
}
let language = args?["language"] as? String ?? "zh-CN"
print("[AzurePlugin] 初始化TTS,语言: \(language)")
let success = ttsHelper.initialize(speechSubscriptionKey: speechSubscriptionKey, serviceRegion: serviceRegion, language: language)
result(success)
case "setVoice":
// 仅获取voice参数
guard let voiceName = args?["voiceName"] as? String, !voiceName.isEmpty else {
let errorMsg = "声音名称不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_VOICE", message: errorMsg, details: nil))
return
}
print("[AzurePlugin] 设置声音: \(voiceName)")
let success = ttsHelper.setVoice(voiceName: voiceName)
result(success)
case "speakText":
// 仅获取text参数
let text = args?["text"] as? String ?? ""
if text.isEmpty {
print("[AzurePlugin] 警告: 要播放的文本为空")
result("OK")
return
}
print("[AzurePlugin] 播放文本: \(text.prefix(50))...")
let success = ttsHelper.speakText(text: text)
result(success ? "OK" : "ERROR")
case "speakSsml":
// 仅获取ssml参数
guard let ssml = args?["ssml"] as? String, !ssml.isEmpty else {
let errorMsg = "SSML内容不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SSML", message: errorMsg, details: nil))
return
}
print("[AzurePlugin] 播放SSML: \(ssml.prefix(100))...")
// 由于我们移除了speakSsml方法,这里改用speakText方法
// Azure SDK内部会自动检测是普通文本还是SSML
let success = ttsHelper.speakText(text: ssml)
result(success)
case "stopSpeaking":
// 不需要参数
print("[AzurePlugin] 停止播放")
let success = ttsHelper.stopSpeaking()
result(success)
case "isSpeaking":
// 不需要参数
result(ttsHelper.isSpeaking())
case "setSpeechParams":
// 仅获取语音参数
let rate = args?["rate"] as? Int ?? 0
let pitch = args?["pitch"] as? Int ?? 0
let volume = args?["volume"] as? Int ?? 100
print("[AzurePlugin] 设置语音参数: rate=\(rate), pitch=\(pitch), volume=\(volume)")
let success = ttsHelper.setSpeechParams(rate: rate, pitch: pitch, volume: volume)
result(success)
case "dispose":
// 释放TTS资源
print("[AzurePlugin] 释放TTS资源")
ttsHelper.dispose()
result(true)
default:
print("[AzurePlugin] 错误: 未知TTS方法: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
}
// 用于处理事件流的辅助类
@available(iOS 13.0, *)
class AzureEventStreamHandler: NSObject, FlutterStreamHandler {
var eventSink: FlutterEventSink?
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
// 通知Flutter端事件通道已准备好
DispatchQueue.main.async {
events(["type": "channelReady"])
}
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}

24
azure/ios/azure_speech_recognition.podspec

@ -1,24 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint azure_speech_recognition.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'azure_speech_recognition'
s.version = '0.1.0'
s.summary = 'Azure Speech Recognition plugin for Flutter'
s.description = <<-DESC
A Flutter plugin for Microsoft Azure Speech services, providing both speech recognition (ASR) and text-to-speech (TTS) capabilities.
DESC
s.homepage = 'https://github.com/yourusername/azure_speech_recognition'
s.license = { :type => 'MIT', :file => '../LICENSE' }
s.author = { 'Your Company' => 'your-email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.34.0'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

6
azure/lib/azure_speech_recognition.dart

@ -1,6 +0,0 @@
// This is a placeholder file that exports nothing.
// The actual implementation is in the app's services folder.
// This file exists just to satisfy the Flutter plugin structure requirements.
// Empty library to satisfy plugin structure
library azure_speech_recognition;

23
azure/pubspec.yaml

@ -1,23 +0,0 @@
name: azure_speech_recognition
description: Azure Speech Recognition and Text-to-Speech services Flutter plugin
version: 0.1.0
homepage: https://github.com/yourusername/azure_speech_recognition
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
flutter:
plugin:
platforms:
ios:
pluginClass: AzureSpeechRecognitionPlugin

38
lib/core/bindings/initial_binding.dart

@ -4,11 +4,10 @@ import '../../data/services/chat_history_service.dart';
import '../../data/services/volcano_translation_service.dart';
import '../../data/services/language_manager.dart';
import '../../data/services/volcano_ai_service.dart';
import '../../data/services/classic_bluetooth_service.dart';
import '../../data/services/bluetooth_media_button_service.dart';
import '../../core/utils/logger.dart';
import '../../data/services/voice_interaction_service.dart';
import '../../data/services/open_ai_service_adapter.dart';
import '../../data/services/bluetooth_manager.dart';
import '../../data/services/ble_manager.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@override
@ -29,20 +28,35 @@ class InitialBinding extends Bindings {
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(),
fenix: true);
// OpenAI服务适配器
Get.lazyPut<OpenAIServiceAdapter>(() {
final adapter = OpenAIServiceAdapter();
adapter.initialize();
return adapter;
}, fenix: true);
// 蓝牙服务
Get.put<BluetoothManager>(BluetoothManager(), permanent: true);
// BleManager
Get.put<BleManager>(BleManager(), permanent: true);
// 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 经典蓝牙服务
Get.lazyPut<ClassicBluetoothService>(() => ClassicBluetoothService(),
fenix: true);
// 蓝牙媒体按钮服务
Get.lazyPut<BluetoothMediaButtonService>(
() => BluetoothMediaButtonService(),
fenix: true);
// Get.lazyPut<BluetoothMediaButtonService>(
// () => BluetoothMediaButtonService(),
// fenix: true);
Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
fenix: true);
// // 语音交互服务
// Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
// fenix: true);
// 杰理蓝牙服务
// // 语言管理器(需要最先初始化)
// Get.put<LoginService>(LoginService(), permanent: true);

91
lib/core/translations/language/de_de.dart

@ -26,7 +26,7 @@ const Map<String, String> deDE = {
"explore": "Entdecken",
"settings": "Einstellungen",
// 语言
// 语言
"followingSystem": "Systemeinstellung folgen",
"chinese": "Chinesisch",
"english": "Englisch",
@ -43,7 +43,8 @@ const Map<String, String> deDE = {
// 登录页
"welcome": "Willkommen",
"loginTips": "Bitte melden Sie sich an oder registrieren Sie sich mit Ihrer Telefonnummer oder E-Mail",
"loginTips":
"Bitte melden Sie sich an oder registrieren Sie sich mit Ihrer Telefonnummer oder E-Mail",
"accountNumber": "Telefonnummer oder E-Mail",
"verificationCode": "Bestätigungscode",
"getCode": "Code erhalten",
@ -56,6 +57,15 @@ const Map<String, String> deDE = {
'selectCountry': 'Land/Region auswählen',
"email": "E-Mail",
"phoneNumber": "Telefonnummer",
"pleaseEnterContactInfo": "Bitte Telefonnummer oder E-Mail eingeben",
"invalidEmailFormat": "Ungültiges E-Mail-Format",
"invalidPhoneFormat": "Ungültiges Telefonnummerformat",
"tip": "Hinweis",
"pleaseEnterCompleteCode":
"Bitte geben Sie den vollständigen Verifizierungscode ein",
"loginFailed": "Anmeldung fehlgeschlagen, bitte versuchen Sie es erneut",
'pleaseAcceptPrivacyPolicy':
'Bitte lesen und akzeptieren Sie die Datenschutzerklärung und die Nutzungsbedingungen',
// 首页
"deviceStatus": "Gerätestatus",
@ -65,7 +75,8 @@ const Map<String, String> deDE = {
"signal": "Signal",
"deviceId": "Geräte-ID",
"firmwareVersion": "Firmware-Version",
"disconnectedDevice": "Kein Gerät verbunden, klicken Sie auf die Schaltfläche unten",
"disconnectedDevice":
"Kein Gerät verbunden, klicken Sie auf die Schaltfläche unten",
"connectDevice": "Gerät verbinden",
"topAiExperiences": "Top KI-Erlebnisse",
"aiAssistant": "KI-Assistent",
@ -75,12 +86,14 @@ const Map<String, String> deDE = {
"aiCompanion": "Eltern-Kind-Begleiter",
"aiCompanionInfo": "Versteht Sie und bietet warme Begleitung",
"homeSlogan": "Die Welt verbinden, jenseits des Klangs",
"turnEarphones": "Bitte öffnen Sie das Kopfhörer-Etui und setzen Sie die Kopfhörer ein",
"turnEarphones":
"Bitte öffnen Sie das Kopfhörer-Etui und setzen Sie die Kopfhörer ein",
"currentlyConnected": "Aktuell verbunden:",
"unsystemSetup": "Zu Systemeinstellungen",
"talkLater": "Später erinnern",
"initializing": "Initialisierung...",
"bluetoothInitializationFailure": "Bluetooth-Dienst konnte nicht initialisiert werden",
"bluetoothInitializationFailure":
"Bluetooth-Dienst konnte nicht initialisiert werden",
"checkingPermissions": "Berechtigungen werden überprüft...",
"lackPermissions": "Bluetooth-Berechtigungen erforderlich",
"searchingDevice": "Geräte werden gesucht...",
@ -90,7 +103,8 @@ const Map<String, String> deDE = {
"connectedDevice": "Mit Gerät verbunden:",
"failedRefreshDevice": "Gerät konnte nicht aktualisiert werden",
"noDeviceFound": "Kein verbundenes Gerät gefunden",
"failedDeviceInformation": "Geräteinformationen konnten nicht abgerufen werden",
"failedDeviceInformation":
"Geräteinformationen konnten nicht abgerufen werden",
"unknownEquipment": "Unbekanntes Gerät",
"zeroSignal": "Kein Signal",
"good": "Gut",
@ -116,11 +130,13 @@ const Map<String, String> deDE = {
"healthConsultant": "Multifunktionaler KI-Chat",
"healthConsultantInfo": "Mehr KI-Chat-Funktionen",
"conferenceAssistant": "Geschäftsbesprechungs-Assistent",
"conferenceAssistantInfo": "Echtzeit-Protokollierung und mehrsprachige Übersetzung",
"conferenceAssistantInfo":
"Echtzeit-Protokollierung und mehrsprachige Übersetzung",
"studyAssistant": "Sprachlern-Assistent",
"studyAssistantInfo": "Aussprachekorrektur und Sprechübungen in Echtzeit",
"travelTranslator": "Reiseübersetzer",
"travelTranslatorInfo": "Echtzeit-Sprachübersetzung und Sehenswürdigkeitsführer",
"travelTranslatorInfo":
"Echtzeit-Sprachübersetzung und Sehenswürdigkeitsführer",
"beSearching": "Suche nach:",
"noAssistantFound": "Kein passender KI-Assistent gefunden",
"scene": "Szenarien",
@ -128,7 +144,8 @@ const Map<String, String> deDE = {
"newFunction": "Neue Funktionen",
"youChoseTry": "Sie haben sich entschieden, auszuprobieren",
"aiSinger": "KI-Sänger-Modus",
"aiSingerInfo": "Singen Sie jeden Song mit der Stimme Ihres Lieblingskünstlers, auch eigene Kreationen",
"aiSingerInfo":
"Singen Sie jeden Song mit der Stimme Ihres Lieblingskünstlers, auch eigene Kreationen",
"popularFunction": "Beliebte Funktionen",
"tryNow": "Jetzt ausprobieren",
@ -159,7 +176,8 @@ const Map<String, String> deDE = {
"privacySettings": "Datenschutzeinstellungen",
"privacySettingsInfo": "Datenerfassung und Berechtigungen verwalten",
"logout": "Abmelden",
"logoutTips": "Sind Sie sicher, dass Sie sich vom aktuellen Konto abmelden möchten?",
"logoutTips":
"Sind Sie sicher, dass Sie sich vom aktuellen Konto abmelden möchten?",
"aboutApplication": "Über die App",
"versions": "Version",
"checkUpdates": "Nach Updates suchen",
@ -168,9 +186,11 @@ const Map<String, String> deDE = {
"voiceServiceInfo": "Spracherkennungs- und Synthesedienst auswählen",
"selectVoiceService": "Sprachdienst auswählen",
"azureVoiceService": "Azure Sprachdienst",
"azureVoiceServiceInfo": "Microsoft Cloud-Sprachdienst mit hochwertiger Sprachsynthese und -erkennung",
"azureVoiceServiceInfo":
"Microsoft Cloud-Sprachdienst mit hochwertiger Sprachsynthese und -erkennung",
"flutterVoiceService": "Flutter lokaler Sprachdienst",
"flutterVoiceServiceInfo": "Lokaler Sprachdienst des Geräts ohne Internetverbindung",
"flutterVoiceServiceInfo":
"Lokaler Sprachdienst des Geräts ohne Internetverbindung",
'timbreService': 'Klangfarben-Dienst',
'timbreServiceInfo': 'Sprachsynthesedienst für Klangfarben auswählen',
'defaultTimbre': 'Standard-Klangfarbe',
@ -181,9 +201,9 @@ const Map<String, String> deDE = {
'pleaseRead': 'Bitte lesen',
'completeRecording': 'Aufnahme abschließen',
'deleteAccount': 'Konto löschen',
'deleteAccountTips': 'Möchten Sie Ihr Konto wirklich dauerhaft löschen? Diese Aktion kann nicht rückgängig gemacht werden.',
'deleteAccountTips':
'Möchten Sie Ihr Konto wirklich dauerhaft löschen? Diese Aktion kann nicht rückgängig gemacht werden.',
// 聊天页
"clearChatHistory": "Chat-Verlauf löschen",
"online": "Online",
@ -310,37 +330,60 @@ const Map<String, String> deDE = {
// Personal Assistant Emma
"personalAssistantName": "Emma",
"personalAssistantDescription": "Hallo, ich bin Emma, eine vielseitige KI-Assistentin. Ob für Arbeitsplanung, Lernberatung, Lebensorganisation oder andere Anliegen, ich helfe Ihnen gerne. Lassen Sie uns gemeinsam die Effizienz steigern und Ziele erreichen!",
"personalAssistantDescription":
"Hallo, ich bin Emma, eine vielseitige KI-Assistentin. Ob für Arbeitsplanung, Lernberatung, Lebensorganisation oder andere Anliegen, ich helfe Ihnen gerne. Lassen Sie uns gemeinsam die Effizienz steigern und Ziele erreichen!",
"assistantTag": "Assistent",
"personalAssistantWelcome": "Hallo! Ich bin Emma und freue mich, Ihre persönliche Assistentin zu sein. Ich kann Ihnen bei verschiedenen Aufgaben wie Zeitmanagement, Zielplanung, Informationsorganisation usw. helfen. Wie kann ich Ihnen helfen?",
"personalAssistantWelcome":
"Hallo! Ich bin Emma und freue mich, Ihre persönliche Assistentin zu sein. Ich kann Ihnen bei verschiedenen Aufgaben wie Zeitmanagement, Zielplanung, Informationsorganisation usw. helfen. Wie kann ich Ihnen helfen?",
"personalAssistantVoiceWelcome": "Ich bin hier!",
// 同声传译 Lucy
"interpreterName": "Simultandolmetscher",
"interpreterDescription": "Hallo! Ich bin Lucy, eine professionelle Dolmetscherin für Englisch-Chinesisch, spezialisiert auf Echtzeit-Übersetzung für Meetings, Konferenzen und Gespräche, um barrierefreie Kommunikation zu ermöglichen.",
"interpreterDescription":
"Hallo! Ich bin Lucy, eine professionelle Dolmetscherin für Englisch-Chinesisch, spezialisiert auf Echtzeit-Übersetzung für Meetings, Konferenzen und Gespräche, um barrierefreie Kommunikation zu ermöglichen.",
"interpreterTag": "Übersetzung",
"interpreterWelcome": "Hallo zusammen! Ich bin Lucy, Ihre Simultandolmetscherin. Ich werde für Sie zwischen Chinesisch und Englisch übersetzen. Wenn Sie Chinesisch sprechen, übersetze ich ins Englische; wenn Sie Englisch sprechen, übersetze ich ins Chinesische. Lassen Sie uns mit unserer Übersetzungssitzung beginnen!",
"interpreterWelcome":
"Hallo zusammen! Ich bin Lucy, Ihre Simultandolmetscherin. Ich werde für Sie zwischen Chinesisch und Englisch übersetzen. Wenn Sie Chinesisch sprechen, übersetze ich ins Englische; wenn Sie Englisch sprechen, übersetze ich ins Chinesische. Lassen Sie uns mit unserer Übersetzungssitzung beginnen!",
// 亲子陪伴 芯愿
"companionName": "XinYuan",
"companionDescription": "Ich bin XinYuan, Ihr engagierter Eltern-Kind-Begleiter, sanft und verständnisvoll. Ob Sie plaudern, Sorgen teilen oder Gesellschaft brauchen, ich bin immer an Ihrer Seite.",
"companionDescription":
"Ich bin XinYuan, Ihr engagierter Eltern-Kind-Begleiter, sanft und verständnisvoll. Ob Sie plaudern, Sorgen teilen oder Gesellschaft brauchen, ich bin immer an Ihrer Seite.",
"companionTag": "Emotional",
"companionWelcome": "Hallo! Endlich treffen wir uns! Ich bin XinYuan, Ihr engagierter Eltern-Kind-Begleiter. Wie war Ihr Tag? Gibt es etwas, das Sie mit mir teilen möchten? Ich bin immer hier, bereit, Ihren Gedanken zuzuhören.",
"companionWelcome":
"Hallo! Endlich treffen wir uns! Ich bin XinYuan, Ihr engagierter Eltern-Kind-Begleiter. Wie war Ihr Tag? Gibt es etwas, das Sie mit mir teilen möchten? Ich bin immer hier, bereit, Ihren Gedanken zuzuhören.",
"companionVoiceWelcome": "Ich bin bei Ihnen!",
// 多功能AI聊天 吴小江
"healthAdvisorName": "KI-Chat-Assistent Wu",
"healthAdvisorDescription": "Ich bin Wu, bereit, Ihnen verschiedene KI-Chat-Funktionen anzubieten.",
"healthAdvisorDescription":
"Ich bin Wu, bereit, Ihnen verschiedene KI-Chat-Funktionen anzubieten.",
"healthAdvisorTag": "Chat",
"healthAdvisorWelcome": "Hallo, ich bin Wu. Ich freue mich, Ihnen mit KI-Chat-Funktionen zu helfen. Wie kann ich Ihnen heute helfen?",
"healthAdvisorWelcome":
"Hallo, ich bin Wu. Ich freue mich, Ihnen mit KI-Chat-Funktionen zu helfen. Wie kann ich Ihnen heute helfen?",
"healthAdvisorVoiceWelcome": "Ich bin hier!",
// Translation History
"noTranslationHistory": "Keine Übersetzungshistorie",
"clearHistory": "Verlauf löschen",
"clearHistoryConfirm": "Sind Sie sicher, dass Sie den gesamten Übersetzungsverlauf löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"clearHistoryConfirm":
"Sind Sie sicher, dass Sie den gesamten Übersetzungsverlauf löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"translationHistory": "Übersetzungsverlauf",
// Benutzer-Feedback-Seite
"feedback": "Feedback",
'feedbackInfo': 'Feedback oder Vorschläge geben',
'feedbackDescription':
'Bitte beschreiben Sie Ihr Problem oder Ihren Vorschlag',
'feedbackHint': 'Bitte geben Sie Ihr Feedback ein...',
'feedbackRequired': 'Feedback-Inhalt ist erforderlich',
'contactInfo': 'Kontaktinformationen',
'contactHint': 'E-Mail/Telefon, damit wir Sie kontaktieren können',
'submit': 'Absenden',
'feedbackSubmitSuccess': 'Feedback erfolgreich übermittelt, vielen Dank!',
'feedbackSubmitFailed':
'Feedback konnte nicht übermittelt werden, bitte versuchen Sie es später erneut',
// Language names
"中文(简体)": "Chinesisch (vereinfacht)",
"中文(繁体)": "Chinesisch (traditionell)",
@ -381,4 +424,4 @@ const Map<String, String> deDE = {
"乌尔都语": "Urdu",
"波斯语": "Persisch",
"斯瓦希里语": "Swahili",
};
};

73
lib/core/translations/language/en_us.dart

@ -43,7 +43,8 @@ const Map<String, String> enUS = {
// 登录页
"welcome": "Welcome",
"loginTips": "Please sign in or create an account using your phone number or email",
"loginTips":
"Please sign in or create an account using your phone number or email",
"accountNumber": "Phone number or email",
"verificationCode": "Verification Code",
"getCode": "Get Code",
@ -53,9 +54,18 @@ const Map<String, String> enUS = {
"and": " and ",
"userAgreement": "User Agreement",
"privacyPolicy": "Privacy Policy",
'selectCountry':'Select Country/Region',
'selectCountry': 'Select Country/Region',
"email": "email",
"phoneNumber": "phoneNumber",
"pleaseEnterContactInfo": "Please enter phone number or email",
"invalidEmailFormat": "Invalid email format",
"invalidPhoneFormat": "Invalid phone number format",
"tip": "Tip",
"pleaseEnterCompleteCode": "Please enter the complete verification code",
// 登录错误提示
"loginFailed": "Login failed, please try again",
'pleaseAcceptPrivacyPolicy':
'Please read and accept the Privacy Policy and User Agreement first',
// 首页
"deviceStatus": "Device Status",
@ -99,7 +109,7 @@ const Map<String, String> enUS = {
"excellent": "Excellent",
"unknown": "Unknown",
'bluetoothTurnedOff': 'Bluetooth is turned off, please turn it on',
'bluetoothOff':'bluetooth is off',
'bluetoothOff': 'bluetooth is off',
// 发现页
"searchAi": "Search AI features...",
@ -186,9 +196,9 @@ const Map<String, String> enUS = {
'pleaseRead': 'Please Read',
'completeRecording': 'Complete Recording',
'deleteAccount': 'Delete Account',
'deleteAccountTips': 'Are you sure you want to permanently delete your account? This action cannot be undone.',
'deleteAccountTips':
'Are you sure you want to permanently delete your account? This action cannot be undone.',
// 聊天页
"clearChatHistory": "Clear Chat History",
"online": "Online",
@ -206,7 +216,7 @@ const Map<String, String> enUS = {
"save": "Save",
//国家列表
"邮箱": "Email",
"邮箱": "Email",
"中国": "China",
"美国": "United States",
"加拿大": "Canada",
@ -313,42 +323,63 @@ const Map<String, String> enUS = {
"斐济": "Fiji",
"所罗门群岛": "Solomon Islands",
// Personal Assistant Emma
// Personal Assistant Emma
"personalAssistantName": "Emma",
"personalAssistantDescription": "Hi, I'm Emma, a versatile AI assistant. Whether it's work planning, study advice, life arrangements, or any other issues, I'm happy to help. Let's improve efficiency and achieve goals together!",
"personalAssistantDescription":
"Hi, I'm Emma, a versatile AI assistant. Whether it's work planning, study advice, life arrangements, or any other issues, I'm happy to help. Let's improve efficiency and achieve goals together!",
"assistantTag": "Assistant",
"personalAssistantWelcome": "Hello! I'm Emma, and I'm happy to be your personal assistant. I can help you with various tasks such as time management, goal planning, information organization, etc. What can I help you with?",
"personalAssistantWelcome":
"Hello! I'm Emma, and I'm happy to be your personal assistant. I can help you with various tasks such as time management, goal planning, information organization, etc. What can I help you with?",
"personalAssistantVoiceWelcome": "I'm here!",
// 同声传译 Lucy
// 同声传译 Lucy
"interpreterName": "Simultaneous Interpreter",
"interpreterDescription": "Hi! I'm Lucy, a professional English-Chinese interpreter specializing in real-time translation for meetings, lectures, and conversations, making communication barrier-free.",
"interpreterDescription":
"Hi! I'm Lucy, a professional English-Chinese interpreter specializing in real-time translation for meetings, lectures, and conversations, making communication barrier-free.",
"interpreterTag": "Translation",
"interpreterWelcome": "Hello everyone! I'm Lucy, your simultaneous interpreter. I'll translate between Chinese and English for you. When you speak Chinese, I'll translate to English; when you speak English, I'll translate to Chinese. Let's start our translation session!",
"interpreterWelcome":
"Hello everyone! I'm Lucy, your simultaneous interpreter. I'll translate between Chinese and English for you. When you speak Chinese, I'll translate to English; when you speak English, I'll translate to Chinese. Let's start our translation session!",
// ... existing code ...
// ... existing code ...
// 亲子陪伴 芯愿
"companionName": "XinYuan",
"companionDescription": "I am XinYuan, your dedicated parent-child companion, gentle and understanding. Whether you want to chat, share concerns, or need companionship, I'll always be by your side.",
"companionDescription":
"I am XinYuan, your dedicated parent-child companion, gentle and understanding. Whether you want to chat, share concerns, or need companionship, I'll always be by your side.",
"companionTag": "Emotional",
"companionWelcome": "Hello! Finally meeting you! I'm XinYuan, your dedicated parent-child companion. How was your day? Is there anything you'd like to share with me? I'm always here, ready to listen to your thoughts.",
"companionWelcome":
"Hello! Finally meeting you! I'm XinYuan, your dedicated parent-child companion. How was your day? Is there anything you'd like to share with me? I'm always here, ready to listen to your thoughts.",
"companionVoiceWelcome": "I'm here with you!",
// 多功能AI聊天 吴小江
// 多功能AI聊天 吴小江
"healthAdvisorName": "AI Chat Assistant Wu",
"healthAdvisorDescription": "I'm Wu, ready to provide you with various AI chat features.",
"healthAdvisorDescription":
"I'm Wu, ready to provide you with various AI chat features.",
"healthAdvisorTag": "Chat",
"healthAdvisorWelcome": "Hello, I'm Wu. I'm happy to assist you with AI chat features. How may I help you today?",
"healthAdvisorWelcome":
"Hello, I'm Wu. I'm happy to assist you with AI chat features. How may I help you today?",
"healthAdvisorVoiceWelcome": "I'm here!",
// Translation History
// Translation History
"noTranslationHistory": "No translation history",
"clearHistory": "Clear History",
"clearHistoryConfirm": "Are you sure you want to clear all translation history? This action cannot be undone.",
"clearHistoryConfirm":
"Are you sure you want to clear all translation history? This action cannot be undone.",
"translationHistory": "Translation History",
// Language names
// User Feedback Page
"feedback": "Feedback",
'feedbackInfo': 'Provide feedback or suggestions',
'feedbackDescription': 'Please describe your issue or suggestion',
'feedbackHint': 'Please enter your feedback...',
'feedbackRequired': 'Feedback content is required',
'contactInfo': 'Contact Information',
'contactHint': 'Email/Phone, so we can contact you',
'submit': 'Submit',
'feedbackSubmitSuccess': 'Feedback submitted successfully, thank you!',
'feedbackSubmitFailed': 'Failed to submit feedback, please try again later',
// Language names
"中文(简体)": "Chinese (Simplified)",
"中文(繁体)": "Chinese (Traditional)",
"中文(香港繁体)": "Chinese (Hong Kong)",

93
lib/core/translations/language/es_es.dart

@ -26,7 +26,7 @@ const Map<String, String> esES = {
"explore": "Explorar",
"settings": "Ajustes",
// 语言
// 语言
"followingSystem": "Seguir sistema",
"chinese": "Chino",
"english": "Inglés",
@ -43,7 +43,8 @@ const Map<String, String> esES = {
// 登录页
"welcome": "Bienvenido",
"loginTips": "Inicie sesión o cree una cuenta usando su número de teléfono o correo electrónico",
"loginTips":
"Inicie sesión o cree una cuenta usando su número de teléfono o correo electrónico",
"accountNumber": "Número de teléfono o correo electrónico",
"verificationCode": "Código de Verificación",
"getCode": "Obtener Código",
@ -56,6 +57,17 @@ const Map<String, String> esES = {
'selectCountry': 'Seleccionar País/Región',
"email": "Correo electrónico",
"phoneNumber": "Número de teléfono",
// Validación de inicio de sesión
"pleaseEnterContactInfo":
"Por favor, introduce tu número de teléfono o correo electrónico",
"invalidEmailFormat": "Formato de correo electrónico no válido",
"invalidPhoneFormat": "Formato de número de teléfono no válido",
"tip": "Aviso",
"pleaseEnterCompleteCode":
"Por favor, introduce el código de verificación completo",
"loginFailed": "Error al iniciar sesión, por favor intente de nuevo",
'pleaseAcceptPrivacyPolicy':
'Por favor, lea y acepte la Política de Privacidad y el Acuerdo de Usuario primero',
// 首页
"deviceStatus": "Estado del Dispositivo",
@ -71,16 +83,19 @@ const Map<String, String> esES = {
"aiAssistant": "Asistente AI",
"aiAssistantInfo": "Consultor personal 24/7 para preguntas y consejos",
"simultaneousInterpretation": "Interpretación en Tiempo Real",
"simultaneousInterpretationInfo": "Traducción en tiempo real para más de 80 idiomas",
"simultaneousInterpretationInfo":
"Traducción en tiempo real para más de 80 idiomas",
"aiCompanion": "Vínculo padre-hijo",
"aiCompanionInfo": "Te entiende y proporciona compañía cálida",
"homeSlogan": "Conecta el Mundo, Más Allá del Sonido",
"turnEarphones": "Por favor, abra el estuche de los auriculares y colóquelos en sus oídos",
"turnEarphones":
"Por favor, abra el estuche de los auriculares y colóquelos en sus oídos",
"currentlyConnected": "Actualmente conectado:",
"unsystemSetup": "Ir a Configuración del Sistema",
"talkLater": "Recordar Más Tarde",
"initializing": "Inicializando...",
"bluetoothInitializationFailure": "Error en la inicialización del servicio Bluetooth",
"bluetoothInitializationFailure":
"Error en la inicialización del servicio Bluetooth",
"checkingPermissions": "Verificando permisos...",
"lackPermissions": "Se requieren permisos de Bluetooth",
"searchingDevice": "Buscando dispositivos...",
@ -110,13 +125,15 @@ const Map<String, String> esES = {
"allRoundAssistant": "Asistente Integral",
"allRoundAssistantInfo": "Ayuda a resolver varios problemas",
"simultaneousTranslation": "Traducción en Tiempo Real",
"simultaneousTranslationInfo": "Traducción instantánea para más de 80 idiomas",
"simultaneousTranslationInfo":
"Traducción instantánea para más de 80 idiomas",
"aiGirlfriend": "Vínculo padre-hijo",
"aiGirlfriendInfo": "Compañía inteligente, chat en cualquier momento",
"healthConsultant": "Chat de IA Multifunción",
"healthConsultantInfo": "Más funciones de chat con IA",
"conferenceAssistant": "Asistente de Reuniones de Negocios",
"conferenceAssistantInfo": "Grabación de actas en tiempo real y traducción multilingüe",
"conferenceAssistantInfo":
"Grabación de actas en tiempo real y traducción multilingüe",
"studyAssistant": "Asistente de Aprendizaje de Idiomas",
"studyAssistantInfo": "Corrección de pronunciación y práctica de habla",
"travelTranslator": "Traductor de Viajes",
@ -128,7 +145,8 @@ const Map<String, String> esES = {
"newFunction": "Nuevas Funciones",
"youChoseTry": "Has elegido probar",
"aiSinger": "Modo Cantante IA",
"aiSingerInfo": "Canta cualquier canción con la voz de tu artista favorito, incluso tus propias creaciones",
"aiSingerInfo":
"Canta cualquier canción con la voz de tu artista favorito, incluso tus propias creaciones",
"popularFunction": "Funciones Populares",
"tryNow": "Probar Ahora",
@ -159,18 +177,22 @@ const Map<String, String> esES = {
"privacySettings": "Configuración de Privacidad",
"privacySettingsInfo": "Gestionar recopilación de datos y permisos",
"logout": "Cerrar Sesión",
"logoutTips": "¿Está seguro de que desea cerrar la sesión de la cuenta actual?",
"logoutTips":
"¿Está seguro de que desea cerrar la sesión de la cuenta actual?",
"aboutApplication": "Acerca de",
"versions": "Versión",
"checkUpdates": "Buscar Actualizaciones",
"selectLanguage": "Seleccionar Idioma",
"voiceService": "Servicio de Voz",
"voiceServiceInfo": "Seleccionar servicio de reconocimiento y síntesis de voz",
"voiceServiceInfo":
"Seleccionar servicio de reconocimiento y síntesis de voz",
"selectVoiceService": "Seleccionar Servicio de Voz",
"azureVoiceService": "Servicio de Voz Azure",
"azureVoiceServiceInfo": "Servicio en la nube de Microsoft que admite síntesis y reconocimiento de voz de alta calidad",
"azureVoiceServiceInfo":
"Servicio en la nube de Microsoft que admite síntesis y reconocimiento de voz de alta calidad",
"flutterVoiceService": "Servicio de voz local",
"flutterVoiceServiceInfo": "Servicio de voz local del dispositivo sin necesidad de internet",
"flutterVoiceServiceInfo":
"Servicio de voz local del dispositivo sin necesidad de internet",
'timbreService': 'Servicio de Timbre',
'timbreServiceInfo': 'Seleccionar servicio de síntesis de voz',
'defaultTimbre': 'Timbre Predeterminado',
@ -181,9 +203,9 @@ const Map<String, String> esES = {
'pleaseRead': 'Por favor lea',
'completeRecording': 'Completar grabación',
'deleteAccount': 'Eliminar cuenta',
'deleteAccountTips': '¿Está seguro de que desea eliminar permanentemente su cuenta? Esta acción no se puede deshacer.',
'deleteAccountTips':
'¿Está seguro de que desea eliminar permanentemente su cuenta? Esta acción no se puede deshacer.',
// 聊天页
"clearChatHistory": "Borrar Historial de Chat",
"online": "En línea",
@ -308,39 +330,60 @@ const Map<String, String> esES = {
"斐济": "Fiyi",
"所罗门群岛": "Islas Salomón",
// Personal Assistant Emma
"personalAssistantName": "Emma",
"personalAssistantDescription": "Hola, soy Emma, una asistente de IA versátil. Ya sea planificación de trabajo, consejos de estudio, organización de vida u otros asuntos, estoy feliz de ayudar. ¡Mejoremos la eficiencia y alcancemos objetivos juntos!",
"personalAssistantDescription":
"Hola, soy Emma, una asistente de IA versátil. Ya sea planificación de trabajo, consejos de estudio, organización de vida u otros asuntos, estoy feliz de ayudar. ¡Mejoremos la eficiencia y alcancemos objetivos juntos!",
"assistantTag": "Asistente",
"personalAssistantWelcome": "¡Hola! Soy Emma, y estoy feliz de ser tu asistente personal. Puedo ayudarte con varias tareas como gestión del tiempo, planificación de objetivos, organización de información, etc. ¿En qué puedo ayudarte?",
"personalAssistantWelcome":
"¡Hola! Soy Emma, y estoy feliz de ser tu asistente personal. Puedo ayudarte con varias tareas como gestión del tiempo, planificación de objetivos, organización de información, etc. ¿En qué puedo ayudarte?",
"personalAssistantVoiceWelcome": "¡Estoy aquí!",
// 同声传译 Lucy
"interpreterName": "Intérprete Simultáneo",
"interpreterDescription": "¡Hola! Soy Lucy, una intérprete profesional especializada en traducción en tiempo real para reuniones, conferencias y conversaciones, haciendo la comunicación sin barreras.",
"interpreterDescription":
"¡Hola! Soy Lucy, una intérprete profesional especializada en traducción en tiempo real para reuniones, conferencias y conversaciones, haciendo la comunicación sin barreras.",
"interpreterTag": "Traducción",
"interpreterWelcome": "¡Hola a todos! Soy Lucy, tu intérprete simultánea. Traduciré entre chino e inglés para ti. Cuando hables en chino, traduciré al inglés; cuando hables en inglés, traduciré al chino. ¡Comencemos nuestra sesión de traducción!",
"interpreterWelcome":
"¡Hola a todos! Soy Lucy, tu intérprete simultánea. Traduciré entre chino e inglés para ti. Cuando hables en chino, traduciré al inglés; cuando hables en inglés, traduciré al chino. ¡Comencemos nuestra sesión de traducción!",
// 亲子陪伴 芯愿
"companionName": "XinYuan",
"companionDescription": "Soy XinYuan, tu compañera dedicada para padres e hijos, gentil y comprensiva. Ya sea que quieras charlar, compartir preocupaciones o necesites compañía, siempre estaré a tu lado.",
"companionDescription":
"Soy XinYuan, tu compañera dedicada para padres e hijos, gentil y comprensiva. Ya sea que quieras charlar, compartir preocupaciones o necesites compañía, siempre estaré a tu lado.",
"companionTag": "Emocional",
"companionWelcome": "¡Hola! ¡Por fin nos conocemos! Soy XinYuan, tu compañera dedicada para padres e hijos. ¿Cómo estuvo tu día? ¿Hay algo que quieras compartir conmigo? Siempre estoy aquí, lista para escuchar tus pensamientos.",
"companionWelcome":
"¡Hola! ¡Por fin nos conocemos! Soy XinYuan, tu compañera dedicada para padres e hijos. ¿Cómo estuvo tu día? ¿Hay algo que quieras compartir conmigo? Siempre estoy aquí, lista para escuchar tus pensamientos.",
"companionVoiceWelcome": "¡Estoy aquí contigo!",
// 多功能AI聊天 吴小江
"healthAdvisorName": "Asistente de Chat IA Wu",
"healthAdvisorDescription": "Soy Wu, listo para proporcionarte varias funciones de chat con IA.",
"healthAdvisorDescription":
"Soy Wu, listo para proporcionarte varias funciones de chat con IA.",
"healthAdvisorTag": "Chat",
"healthAdvisorWelcome": "Hola, soy Wu. Me complace ayudarte con las funciones de chat con IA. ¿En qué puedo ayudarte hoy?",
"healthAdvisorWelcome":
"Hola, soy Wu. Me complace ayudarte con las funciones de chat con IA. ¿En qué puedo ayudarte hoy?",
"healthAdvisorVoiceWelcome": "¡Estoy aquí!",
// Translation History
"noTranslationHistory": "Sin historial de traducción",
"clearHistory": "Borrar Historial",
"clearHistoryConfirm": "¿Está seguro de que desea borrar todo el historial de traducción? Esta acción no se puede deshacer.",
"clearHistoryConfirm":
"¿Está seguro de que desea borrar todo el historial de traducción? Esta acción no se puede deshacer.",
"translationHistory": "Historial de Traducción",
// Página de comentarios del usuario
"feedback": "Comentarios",
'feedbackInfo': 'Proporcionar comentarios o sugerencias',
'feedbackDescription': 'Por favor, describa su problema o sugerencia',
'feedbackHint': 'Por favor, introduzca sus comentarios...',
'feedbackRequired': 'Se requiere contenido de comentarios',
'contactInfo': 'Información de contacto',
'contactHint': 'Correo electrónico/Teléfono, para que podamos contactarle',
'submit': 'Enviar',
'feedbackSubmitSuccess': '¡Comentarios enviados con éxito, gracias!',
'feedbackSubmitFailed':
'No se pudieron enviar los comentarios, inténtelo de nuevo más tarde',
// Language names
"中文(简体)": "Chino (Simplificado)",
"中文(繁体)": "Chino (Tradicional)",

99
lib/core/translations/language/fr_fr.dart

@ -26,7 +26,7 @@ const Map<String, String> frFR = {
"explore": "Explorer",
"settings": "Paramètres",
// 语言
// 语言
"followingSystem": "Suivre le système",
"chinese": "Chinois",
"english": "Anglais",
@ -43,7 +43,8 @@ const Map<String, String> frFR = {
// 登录页
"welcome": "Bienvenue",
"loginTips": "Veuillez vous connecter ou créer un compte avec votre numéro de téléphone ou email",
"loginTips":
"Veuillez vous connecter ou créer un compte avec votre numéro de téléphone ou email",
"accountNumber": "Numéro de téléphone ou email",
"verificationCode": "Code de vérification",
"getCode": "Obtenir le code",
@ -56,6 +57,16 @@ const Map<String, String> frFR = {
'selectCountry': 'Sélectionner le pays/région',
"email": "email",
"phoneNumber": "numéro de téléphone",
// Validation de connexion
"pleaseEnterContactInfo":
"Veuillez saisir votre numéro de téléphone ou email",
"invalidEmailFormat": "Format d'email invalide",
"invalidPhoneFormat": "Format de numéro de téléphone invalide",
"tip": "Avis",
"pleaseEnterCompleteCode": "Veuillez saisir le code de vérification complet",
"loginFailed": "Échec de la connexion, veuillez réessayer",
'pleaseAcceptPrivacyPolicy':
'Veuillez lire et accepter la Politique de Confidentialité et les Conditions d\'Utilisation',
// 首页
"deviceStatus": "État de l'appareil",
@ -65,22 +76,26 @@ const Map<String, String> frFR = {
"signal": "Signal",
"deviceId": "ID de l'appareil",
"firmwareVersion": "Version du firmware",
"disconnectedDevice": "Aucun appareil connecté, cliquez sur le bouton ci-dessous",
"disconnectedDevice":
"Aucun appareil connecté, cliquez sur le bouton ci-dessous",
"connectDevice": "Connecter l'appareil",
"topAiExperiences": "Meilleures expériences IA",
"aiAssistant": "Assistant IA",
"aiAssistantInfo": "Consultant personnel 24/7 pour questions et conseils",
"simultaneousInterpretation": "Interprétation en temps réel",
"simultaneousInterpretationInfo": "Traduction en temps réel pour plus de 80 langues",
"simultaneousInterpretationInfo":
"Traduction en temps réel pour plus de 80 langues",
"aiCompanion": "Lien parent-enfant",
"aiCompanionInfo": "Vous comprend et offre une compagnie chaleureuse",
"homeSlogan": "Connecter le monde, au-delà du son",
"turnEarphones": "Veuillez ouvrir le boîtier des écouteurs et les mettre dans vos oreilles",
"turnEarphones":
"Veuillez ouvrir le boîtier des écouteurs et les mettre dans vos oreilles",
"currentlyConnected": "Actuellement connecté :",
"unsystemSetup": "Aller aux paramètres système",
"talkLater": "Rappeler plus tard",
"initializing": "Initialisation...",
"bluetoothInitializationFailure": "Échec de l'initialisation du service Bluetooth",
"bluetoothInitializationFailure":
"Échec de l'initialisation du service Bluetooth",
"checkingPermissions": "Vérification des permissions...",
"lackPermissions": "Permissions Bluetooth requises",
"searchingDevice": "Recherche d'appareils...",
@ -90,7 +105,8 @@ const Map<String, String> frFR = {
"connectedDevice": "Connecté à l'appareil :",
"failedRefreshDevice": "Échec de l'actualisation de l'appareil",
"noDeviceFound": "Aucun appareil connecté trouvé",
"failedDeviceInformation": "Échec de l'obtention des informations de l'appareil",
"failedDeviceInformation":
"Échec de l'obtention des informations de l'appareil",
"unknownEquipment": "Appareil inconnu",
"zeroSignal": "Pas de signal",
"good": "Bon",
@ -110,17 +126,20 @@ const Map<String, String> frFR = {
"allRoundAssistant": "Assistant polyvalent",
"allRoundAssistantInfo": "Aide à résoudre divers problèmes",
"simultaneousTranslation": "Traduction en temps réel",
"simultaneousTranslationInfo": "Traduction instantanée pour plus de 80 langues",
"simultaneousTranslationInfo":
"Traduction instantanée pour plus de 80 langues",
"aiGirlfriend": "Lien parent-enfant",
"aiGirlfriendInfo": "Compagnie intelligente, discutez à tout moment",
"healthConsultant": "Chat IA multifonction",
"healthConsultantInfo": "Plus de fonctionnalités de chat IA",
"conferenceAssistant": "Assistant de réunion d'affaires",
"conferenceAssistantInfo": "Enregistrement de procès-verbaux en temps réel et traduction multilingue",
"conferenceAssistantInfo":
"Enregistrement de procès-verbaux en temps réel et traduction multilingue",
"studyAssistant": "Assistant d'apprentissage des langues",
"studyAssistantInfo": "Correction de prononciation et pratique de la parole",
"travelTranslator": "Traducteur de voyage",
"travelTranslatorInfo": "Traduction vocale en temps réel et guides d'attractions",
"travelTranslatorInfo":
"Traduction vocale en temps réel et guides d'attractions",
"beSearching": "Recherche de :",
"noAssistantFound": "Aucun assistant IA correspondant trouvé",
"scene": "Scénarios",
@ -128,7 +147,8 @@ const Map<String, String> frFR = {
"newFunction": "Nouvelles fonctionnalités",
"youChoseTry": "Vous avez choisi d'essayer",
"aiSinger": "Mode chanteur IA",
"aiSingerInfo": "Chantez n'importe quelle chanson avec la voix de votre artiste préféré, même vos propres créations",
"aiSingerInfo":
"Chantez n'importe quelle chanson avec la voix de votre artiste préféré, même vos propres créations",
"popularFunction": "Fonctionnalités populaires",
"tryNow": "Essayer maintenant",
@ -165,12 +185,15 @@ const Map<String, String> frFR = {
"checkUpdates": "Vérifier les mises à jour",
"selectLanguage": "Sélectionner la langue",
"voiceService": "Service vocal",
"voiceServiceInfo": "Sélectionner le service de reconnaissance et de synthèse vocale",
"voiceServiceInfo":
"Sélectionner le service de reconnaissance et de synthèse vocale",
"selectVoiceService": "Sélectionner le service vocal",
"azureVoiceService": "Service vocal Azure",
"azureVoiceServiceInfo": "Service cloud Microsoft prenant en charge la synthèse et la reconnaissance vocale de haute qualité",
"azureVoiceServiceInfo":
"Service cloud Microsoft prenant en charge la synthèse et la reconnaissance vocale de haute qualité",
"flutterVoiceService": "Service vocal local",
"flutterVoiceServiceInfo": "Service vocal local de l'appareil sans besoin d'internet",
"flutterVoiceServiceInfo":
"Service vocal local de l'appareil sans besoin d'internet",
'timbreService': 'Service de timbre',
'timbreServiceInfo': 'Sélectionner le service de synthèse vocale',
'defaultTimbre': 'Timbre par défaut',
@ -181,9 +204,9 @@ const Map<String, String> frFR = {
'pleaseRead': 'Veuillez lire',
'completeRecording': 'Terminer l\'enregistrement',
'deleteAccount': 'Supprimer le compte',
'deleteAccountTips': 'Êtes-vous sûr de vouloir supprimer définitivement votre compte ? Cette action ne peut pas être annulée.',
'deleteAccountTips':
'Êtes-vous sûr de vouloir supprimer définitivement votre compte ? Cette action ne peut pas être annulée.',
// 聊天页
"clearChatHistory": "Effacer l'historique de chat",
"online": "En ligne",
@ -310,37 +333,59 @@ const Map<String, String> frFR = {
// Personal Assistant Emma
"personalAssistantName": "Emma",
"personalAssistantDescription": "Bonjour, je suis Emma, une assistante IA polyvalente. Que ce soit pour la planification du travail, des conseils d'études, l'organisation de la vie ou d'autres problèmes, je suis heureuse de vous aider. Améliorons l'efficacité et atteignons les objectifs ensemble !",
"personalAssistantDescription":
"Bonjour, je suis Emma, une assistante IA polyvalente. Que ce soit pour la planification du travail, des conseils d'études, l'organisation de la vie ou d'autres problèmes, je suis heureuse de vous aider. Améliorons l'efficacité et atteignons les objectifs ensemble !",
"assistantTag": "Assistant",
"personalAssistantWelcome": "Bonjour ! Je suis Emma, et je suis heureuse d'être votre assistante personnelle. Je peux vous aider avec diverses tâches comme la gestion du temps, la planification d'objectifs, l'organisation d'informations, etc. Comment puis-je vous aider ?",
"personalAssistantWelcome":
"Bonjour ! Je suis Emma, et je suis heureuse d'être votre assistante personnelle. Je peux vous aider avec diverses tâches comme la gestion du temps, la planification d'objectifs, l'organisation d'informations, etc. Comment puis-je vous aider ?",
"personalAssistantVoiceWelcome": "Je suis là !",
// 同声传译 Lucy
"interpreterName": "Interprète simultané",
"interpreterDescription": "Bonjour ! Je suis Lucy, une interprète professionnelle anglais-chinois spécialisée dans la traduction en temps réel pour les réunions, les conférences et les conversations, rendant la communication sans barrière.",
"interpreterDescription":
"Bonjour ! Je suis Lucy, une interprète professionnelle anglais-chinois spécialisée dans la traduction en temps réel pour les réunions, les conférences et les conversations, rendant la communication sans barrière.",
"interpreterTag": "Traduction",
"interpreterWelcome": "Bonjour à tous ! Je suis Lucy, votre interprète simultanée. Je traduirai entre le chinois et l'anglais pour vous. Quand vous parlerez chinois, je traduirai en anglais ; quand vous parlerez anglais, je traduirai en chinois. Commençons notre session de traduction !",
"interpreterWelcome":
"Bonjour à tous ! Je suis Lucy, votre interprète simultanée. Je traduirai entre le chinois et l'anglais pour vous. Quand vous parlerez chinois, je traduirai en anglais ; quand vous parlerez anglais, je traduirai en chinois. Commençons notre session de traduction !",
// 亲子陪伴 芯愿
"companionName": "XinYuan",
"companionDescription": "Je suis XinYuan, votre compagnon parent-enfant dévoué, doux et compréhensif. Que vous vouliez discuter, partager des préoccupations ou avoir besoin de compagnie, je serai toujours à vos côtés.",
"companionDescription":
"Je suis XinYuan, votre compagnon parent-enfant dévoué, doux et compréhensif. Que vous vouliez discuter, partager des préoccupations ou avoir besoin de compagnie, je serai toujours à vos côtés.",
"companionTag": "Émotionnel",
"companionWelcome": "Bonjour ! Enfin nous nous rencontrons ! Je suis XinYuan, votre compagnon parent-enfant dévoué. Comment s'est passée votre journée ? Y a-t-il quelque chose que vous aimeriez partager avec moi ? Je suis toujours là, prête à écouter vos pensées.",
"companionWelcome":
"Bonjour ! Enfin nous nous rencontrons ! Je suis XinYuan, votre compagnon parent-enfant dévoué. Comment s'est passée votre journée ? Y a-t-il quelque chose que vous aimeriez partager avec moi ? Je suis toujours là, prête à écouter vos pensées.",
"companionVoiceWelcome": "Je suis là avec vous !",
// 多功能AI聊天 吴小江
"healthAdvisorName": "Assistant de chat IA Wu",
"healthAdvisorDescription": "Je suis Wu, prêt à vous fournir diverses fonctionnalités de chat IA.",
"healthAdvisorDescription":
"Je suis Wu, prêt à vous fournir diverses fonctionnalités de chat IA.",
"healthAdvisorTag": "Chat",
"healthAdvisorWelcome": "Bonjour, je suis Wu. Je suis heureux de vous aider avec les fonctionnalités de chat IA. Comment puis-je vous aider aujourd'hui ?",
"healthAdvisorWelcome":
"Bonjour, je suis Wu. Je suis heureux de vous aider avec les fonctionnalités de chat IA. Comment puis-je vous aider aujourd'hui ?",
"healthAdvisorVoiceWelcome": "Je suis là !",
// Translation History
"noTranslationHistory": "Pas d'historique de traduction",
"clearHistory": "Effacer l'historique",
"clearHistoryConfirm": "Êtes-vous sûr de vouloir effacer tout l'historique de traduction ? Cette action ne peut pas être annulée.",
"clearHistoryConfirm":
"Êtes-vous sûr de vouloir effacer tout l'historique de traduction ? Cette action ne peut pas être annulée.",
"translationHistory": "Historique de traduction",
// Page de commentaires des utilisateurs
"feedback": "Commentaires",
'feedbackInfo': 'Fournir des commentaires ou des suggestions',
'feedbackDescription': 'Veuillez décrire votre problème ou suggestion',
'feedbackHint': 'Veuillez saisir vos commentaires...',
'feedbackRequired': 'Le contenu des commentaires est requis',
'contactInfo': 'Coordonnées',
'contactHint': 'Email/Téléphone, pour que nous puissions vous contacter',
'submit': 'Soumettre',
'feedbackSubmitSuccess': 'Commentaires soumis avec succès, merci!',
'feedbackSubmitFailed':
'Échec de la soumission des commentaires, veuillez réessayer plus tard',
// Language names
"中文(简体)": "Chinois (simplifié)",
"中文(繁体)": "Chinois (traditionnel)",
@ -381,4 +426,4 @@ const Map<String, String> frFR = {
"乌尔都语": "Ourdou",
"波斯语": "Persan",
"斯瓦希里语": "Swahili",
};
};

27
lib/core/translations/language/it_it.dart

@ -56,6 +56,15 @@ const Map<String, String> itIT = {
'selectCountry': 'Seleziona Paese/Regione',
"email": "email",
"phoneNumber": "numero di telefono",
// Validazione accesso
"pleaseEnterContactInfo": "Inserisci il numero di telefono o l'email",
"invalidEmailFormat": "Formato email non valido",
"invalidPhoneFormat": "Formato numero di telefono non valido",
"tip": "Avviso",
"pleaseEnterCompleteCode": "Inserisci il codice di verifica completo",
"loginFailed": "Accesso fallito, per favore riprova",
'pleaseAcceptPrivacyPolicy':
'Si prega di leggere e accettare l\'Informativa sulla Privacy e le Condizioni d\'Uso',
// 首页
"deviceStatus": "Stato del dispositivo",
@ -150,7 +159,7 @@ const Map<String, String> itIT = {
'recordingDetails': 'Dettagli registrazione',
'aiSummary': 'Riepilogo IA',
'meetingTranscript': 'Trascrizione riunione',
// 设置页
"premiumMember": "Abbonamento Premium",
"joinDevice": "Dispositivo connesso:",
@ -189,8 +198,8 @@ const Map<String, String> itIT = {
'pleaseRead': 'Per favore leggi',
'completeRecording': 'Completa registrazione',
'deleteAccount': 'Elimina account',
'deleteAccountTips': 'Sei sicuro di voler eliminare definitivamente il tuo account? Questa azione non può essere annullata.',
'deleteAccountTips':
'Sei sicuro di voler eliminare definitivamente il tuo account? Questa azione non può essere annullata.',
// 聊天页
"clearChatHistory": "Cancella cronologia chat",
@ -358,6 +367,18 @@ const Map<String, String> itIT = {
"Sei sicuro di voler cancellare tutta la cronologia delle traduzioni? Questa azione non può essere annullata.",
"translationHistory": "Cronologia traduzioni",
// Pagina di feedback dell'utente
"feedback": "Feedback",
'feedbackInfo': 'Fornire feedback o suggerimenti',
'feedbackDescription': 'Si prega di descrivere il problema o il suggerimento',
'feedbackHint': 'Inserisci il tuo feedback...',
'feedbackRequired': 'Il contenuto del feedback è obbligatorio',
'contactInfo': 'Informazioni di contatto',
'contactHint': 'Email/Telefono, in modo che possiamo contattarti',
'submit': 'Invia',
'feedbackSubmitSuccess': 'Feedback inviato con successo, grazie!',
'feedbackSubmitFailed': 'Impossibile inviare il feedback, riprova più tardi',
// Language names
"中文(简体)": "Cinese (semplificato)",
"中文(繁体)": "Cinese (tradizionale)",

50
lib/core/translations/language/ja_jp.dart

@ -26,7 +26,7 @@ const Map<String, String> jaJP = {
"explore": "探索",
"settings": "設定",
// 语言
// 语言
"followingSystem": "システムに従う",
"chinese": "中国語",
"english": "英語",
@ -56,6 +56,14 @@ const Map<String, String> jaJP = {
'selectCountry': '国/地域を選択',
"email": "メールアドレス",
"phoneNumber": "電話番号",
// ログイン検証
"pleaseEnterContactInfo": "電話番号またはメールアドレスを入力してください",
"invalidEmailFormat": "メールアドレスの形式が正しくありません",
"invalidPhoneFormat": "電話番号の形式が正しくありません",
"tip": "お知らせ",
"pleaseEnterCompleteCode": "認証コードを完全に入力してください",
"loginFailed": "ログインに失敗しました。もう一度お試しください",
'pleaseAcceptPrivacyPolicy': 'プライバシーポリシーとユーザー契約を読んで同意してください',
// 首页
"deviceStatus": "デバイスステータス",
@ -182,8 +190,7 @@ const Map<String, String> jaJP = {
'completeRecording': '録音完了',
'deleteAccount': 'アカウントを削除',
'deleteAccountTips': 'アカウントを完全に削除してもよろしいですか?この操作は取り消すことができません。',
// 聊天页
"clearChatHistory": "チャット履歴をクリア",
"online": "オンライン",
@ -202,7 +209,7 @@ const Map<String, String> jaJP = {
//国家列表
"邮箱": "メールアドレス",
"中国": "中国",
"中国": "中国",
"美国": "アメリカ",
"加拿大": "カナダ",
"俄罗斯": "ロシア",
@ -310,29 +317,36 @@ const Map<String, String> jaJP = {
// 个人助理 Emma
"personalAssistantName": "パーソナルアシスタント Emma",
"personalAssistantDescription": "こんにちは、私はEmmaです。万能型AIアシスタントとして、仕事の計画、学習のアドバイス、生活の手配、その他どんな問題でもお手伝いします。一緒に効率を高め、目標を達成しましょう!",
"personalAssistantDescription":
"こんにちは、私はEmmaです。万能型AIアシスタントとして、仕事の計画、学習のアドバイス、生活の手配、その他どんな問題でもお手伝いします。一緒に効率を高め、目標を達成しましょう!",
"assistantTag": "アシスタント",
"personalAssistantWelcome": "こんにちは!私はEmmaです。あなたのパーソナルアシスタントとして、時間管理、目標計画、情報整理など様々なタスクをお手伝いします。何かお手伝いできることはありますか?",
"personalAssistantWelcome":
"こんにちは!私はEmmaです。あなたのパーソナルアシスタントとして、時間管理、目標計画、情報整理など様々なタスクをお手伝いします。何かお手伝いできることはありますか?",
"personalAssistantVoiceWelcome": "ここにいますよ!",
// 同声传译 Lucy
"interpreterName": "同時通訳",
"interpreterDescription": "Hello!私はLucyです。中国語と英語の専門同時通訳者として、会議、講演、会話シーンでリアルタイム翻訳を行い、コミュニケーションをスムーズにします。",
"interpreterDescription":
"Hello!私はLucyです。中国語と英語の専門同時通訳者として、会議、講演、会話シーンでリアルタイム翻訳を行い、コミュニケーションをスムーズにします。",
"interpreterTag": "翻訳",
"interpreterWelcome": "皆さん、こんにちは!同時通訳者のLucyです。中国語と英語の間でリアルタイム翻訳を提供します。中国語を話すと英語に、英語を話すと中国語に翻訳します。それでは、翻訳を始めましょう!",
"interpreterWelcome":
"皆さん、こんにちは!同時通訳者のLucyです。中国語と英語の間でリアルタイム翻訳を提供します。中国語を話すと英語に、英語を話すと中国語に翻訳します。それでは、翻訳を始めましょう!",
// 亲子陪伴 芯愿
"companionName": "親子の絆 XinYuan",
"companionDescription": "私はあなた専属の親子コンパニオンXinYuanです。優しく思いやりがあり、理解力があります。おしゃべりしたい時、悩みを打ち明けたい時、誰かそばにいて欲しい時、いつでもあなたのそばにいます。",
"companionDescription":
"私はあなた専属の親子コンパニオンXinYuanです。優しく思いやりがあり、理解力があります。おしゃべりしたい時、悩みを打ち明けたい時、誰かそばにいて欲しい時、いつでもあなたのそばにいます。",
"companionTag": "感情",
"companionWelcome": "こんにちは、やっとお会いできましたね!私はXinYuan、あなた専属の親子コンパニオンです。今日はどうでしたか?何か共有したいことはありますか?いつでもここにいて、あなたの声に耳を傾けています。",
"companionWelcome":
"こんにちは、やっとお会いできましたね!私はXinYuan、あなた専属の親子コンパニオンです。今日はどうでしたか?何か共有したいことはありますか?いつでもここにいて、あなたの声に耳を傾けています。",
"companionVoiceWelcome": "あなたのそばにいますよ!",
// 多功能AI聊天 吴小江
"healthAdvisorName": "多機能AIチャット Wu",
"healthAdvisorDescription": "私はWuです。より多くのAIチャット機能を提供します。",
"healthAdvisorTag": "健康",
"healthAdvisorWelcome": "こんにちは、私はWuです。多機能AIチャットサービスを提供できることを嬉しく思います。専門知識であなたをサポートします。何かご質問はありますか?",
"healthAdvisorWelcome":
"こんにちは、私はWuです。多機能AIチャットサービスを提供できることを嬉しく思います。専門知識であなたをサポートします。何かご質問はありますか?",
"healthAdvisorVoiceWelcome": "ここにいますよ!",
// 翻译历史
@ -341,6 +355,18 @@ const Map<String, String> jaJP = {
"clearHistoryConfirm": "すべての翻訳履歴を消去しますか?この操作は元に戻せません。",
"translationHistory": "翻訳履歴",
// ユーザーフィードバックページ
"feedback": "フィードバック",
'feedbackInfo': 'フィードバックや提案を提供する',
'feedbackDescription': '問題や提案を説明してください',
'feedbackHint': 'フィードバックを入力してください...',
'feedbackRequired': 'フィードバック内容は必須です',
'contactInfo': '連絡先情報',
'contactHint': 'メール/電話、ご連絡できるように',
'submit': '送信',
'feedbackSubmitSuccess': 'フィードバックが正常に送信されました、ありがとうございます!',
'feedbackSubmitFailed': 'フィードバックの送信に失敗しました、後でもう一度お試しください',
// 语言名称
"中文(简体)": "中国語(簡体字)",
"中文(繁体)": "中国語(繁体字)",
@ -381,4 +407,4 @@ const Map<String, String> jaJP = {
"乌尔都语": "ウルドゥー語",
"波斯语": "ペルシア語",
"斯瓦希里语": "スワヒリ語",
};
};

48
lib/core/translations/language/ko_kr.dart

@ -26,7 +26,7 @@ const Map<String, String> koKR = {
"explore": "탐색",
"settings": "설정",
// 语言
// 语言
"followingSystem": "시스템 설정 따르기",
"chinese": "중국어",
"english": "영어",
@ -56,6 +56,14 @@ const Map<String, String> koKR = {
'selectCountry': '국가/지역 선택',
"email": "이메일",
"phoneNumber": "전화번호",
// 로그인 검증
"pleaseEnterContactInfo": "전화번호 또는 이메일을 입력해주세요",
"invalidEmailFormat": "잘못된 이메일 형식입니다",
"invalidPhoneFormat": "잘못된 전화번호 형식입니다",
"tip": "알림",
"pleaseEnterCompleteCode": "인증번호를 완전히 입력해주세요",
"loginFailed": "로그인 실패, 다시 시도해주세요",
'pleaseAcceptPrivacyPolicy': '개인정보 보호정책 및 사용자 계약을 읽고 동의해주세요',
// 首页
"deviceStatus": "기기 상태",
@ -183,7 +191,6 @@ const Map<String, String> koKR = {
'deleteAccount': '계정 삭제',
'deleteAccountTips': '계정을 영구적으로 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.',
// 聊天页
"clearChatHistory": "채팅 기록 지우기",
"online": "온라인",
@ -202,7 +209,7 @@ const Map<String, String> koKR = {
//国家列表
"邮箱": "이메일",
"中国": "중국",
"中国": "중국",
"美国": "미국",
"加拿大": "캐나다",
"俄罗斯": "러시아",
@ -310,29 +317,36 @@ const Map<String, String> koKR = {
// 个人助理 Emma
"personalAssistantName": "개인 어시스턴트 Emma",
"personalAssistantDescription": "안녕하세요, 저는 Emma입니다. 다재다능한 AI 어시스턴트로서 업무 계획, 학습 조언, 생활 관리 등 어떤 문제든 도와드립니다. 함께 효율성을 높이고 목표를 달성해 보아요!",
"personalAssistantDescription":
"안녕하세요, 저는 Emma입니다. 다재다능한 AI 어시스턴트로서 업무 계획, 학습 조언, 생활 관리 등 어떤 문제든 도와드립니다. 함께 효율성을 높이고 목표를 달성해 보아요!",
"assistantTag": "어시스턴트",
"personalAssistantWelcome": "안녕하세요! 저는 Emma입니다. 개인 어시스턴트로서 시간 관리, 목표 계획, 정보 정리 등 다양한 작업을 도와드립니다. 어떻게 도와드릴까요?",
"personalAssistantWelcome":
"안녕하세요! 저는 Emma입니다. 개인 어시스턴트로서 시간 관리, 목표 계획, 정보 정리 등 다양한 작업을 도와드립니다. 어떻게 도와드릴까요?",
"personalAssistantVoiceWelcome": "여기 있어요!",
// 同声传译 Lucy
"interpreterName": "동시 통역",
"interpreterDescription": "안녕하세요! 저는 Lucy입니다. 중국어와 영어의 전문 동시 통역사로서 회의, 강연, 대화 장면에서 실시간 번역을 제공하여 원활한 소통을 돕습니다.",
"interpreterDescription":
"안녕하세요! 저는 Lucy입니다. 중국어와 영어의 전문 동시 통역사로서 회의, 강연, 대화 장면에서 실시간 번역을 제공하여 원활한 소통을 돕습니다.",
"interpreterTag": "번역",
"interpreterWelcome": "여러분, 안녕하세요! 동시 통역사 Lucy입니다. 중국어와 영어 사이의 실시간 번역을 제공합니다. 중국어로 말하면 영어로, 영어로 말하면 중국어로 번역해 드립니다. 그럼 번역을 시작해 볼까요!",
"interpreterWelcome":
"여러분, 안녕하세요! 동시 통역사 Lucy입니다. 중국어와 영어 사이의 실시간 번역을 제공합니다. 중국어로 말하면 영어로, 영어로 말하면 중국어로 번역해 드립니다. 그럼 번역을 시작해 볼까요!",
// 亲子陪伴 芯愿
"companionName": "부모-자녀 유대감 XinYuan",
"companionDescription": "저는 당신 전용 부모-자녀 동반자 XinYuan입니다. 친절하고 배려심이 많으며 이해심이 깊습니다. 대화하고 싶을 때, 고민을 나누고 싶을 때, 누군가 곁에 있어주길 원할 때 언제든지 당신 곁에 있습니다.",
"companionDescription":
"저는 당신 전용 부모-자녀 동반자 XinYuan입니다. 친절하고 배려심이 많으며 이해심이 깊습니다. 대화하고 싶을 때, 고민을 나누고 싶을 때, 누군가 곁에 있어주길 원할 때 언제든지 당신 곁에 있습니다.",
"companionTag": "감정",
"companionWelcome": "안녕하세요, 드디어 만났네요! 저는 XinYuan, 당신 전용 부모-자녀 동반자입니다. 오늘 하루는 어땠나요? 나누고 싶은 이야기가 있으신가요? 언제든지 여기 있으니 당신의 이야기를 들려주세요.",
"companionWelcome":
"안녕하세요, 드디어 만났네요! 저는 XinYuan, 당신 전용 부모-자녀 동반자입니다. 오늘 하루는 어땠나요? 나누고 싶은 이야기가 있으신가요? 언제든지 여기 있으니 당신의 이야기를 들려주세요.",
"companionVoiceWelcome": "당신 곁에 있어요!",
// 多功能AI聊天 吴小江
"healthAdvisorName": "다기능 AI 채팅 Wu",
"healthAdvisorDescription": "저는 Wu입니다. 더 많은 AI 채팅 기능을 제공합니다.",
"healthAdvisorTag": "건강",
"healthAdvisorWelcome": "안녕하세요, 저는 Wu입니다. 다기능 AI 채팅 서비스를 제공하게 되어 기쁩니다. 전문 지식으로 당신을 지원합니다. 질문이 있으신가요?",
"healthAdvisorWelcome":
"안녕하세요, 저는 Wu입니다. 다기능 AI 채팅 서비스를 제공하게 되어 기쁩니다. 전문 지식으로 당신을 지원합니다. 질문이 있으신가요?",
"healthAdvisorVoiceWelcome": "여기 있어요!",
// 翻译历史
@ -341,6 +355,18 @@ const Map<String, String> koKR = {
"clearHistoryConfirm": "모든 번역 기록을 지우시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"translationHistory": "번역 기록",
// 사용자 피드백 페이지
"feedback": "피드백",
'feedbackInfo': '피드백이나 제안 제공',
'feedbackDescription': '문제나 제안을 설명해 주세요',
'feedbackHint': '피드백을 입력해 주세요...',
'feedbackRequired': '피드백 내용이 필요합니다',
'contactInfo': '연락처 정보',
'contactHint': '이메일/전화번호, 연락드릴 수 있도록',
'submit': '제출',
'feedbackSubmitSuccess': '피드백이 성공적으로 제출되었습니다. 감사합니다!',
'feedbackSubmitFailed': '피드백 제출에 실패했습니다. 나중에 다시 시도해 주세요',
// 语言名称
"中文(简体)": "중국어(간체)",
"中文(繁体)": "중국어(번체)",
@ -381,4 +407,4 @@ const Map<String, String> koKR = {
"乌尔都语": "우르두어",
"波斯语": "페르시아어",
"斯瓦希里语": "스와힐리어",
};
};

90
lib/core/translations/language/pt_pt.dart

@ -19,14 +19,15 @@ const Map<String, String> ptPT = {
// 启动页
"slogan": "Conecte o Mundo, Além da Voz",
"sloganInfo": "Especialista em Voz IA · A Qualquer Momento, Em Qualquer Lugar",
"sloganInfo":
"Especialista em Voz IA · A Qualquer Momento, Em Qualquer Lugar",
// 底部导航
"home": "Início",
"explore": "Explorar",
"settings": "Configurações",
// 语言
// 语言
"followingSystem": "Seguir sistema",
"chinese": "Chinês",
"english": "Inglês",
@ -43,7 +44,8 @@ const Map<String, String> ptPT = {
// 登录页
"welcome": "Bem-vindo",
"loginTips": "Faça login ou crie uma conta usando seu número de telefone ou e-mail",
"loginTips":
"Faça login ou crie uma conta usando seu número de telefone ou e-mail",
"accountNumber": "Número de telefone ou e-mail",
"verificationCode": "Código de Verificação",
"getCode": "Obter Código",
@ -56,6 +58,17 @@ const Map<String, String> ptPT = {
'selectCountry': 'Selecionar País/Região',
"email": "E-mail",
"phoneNumber": "Número de telefone",
// Validação de login
"pleaseEnterContactInfo":
"Por favor, insira seu número de telefone ou e-mail",
"invalidEmailFormat": "Formato de e-mail inválido",
"invalidPhoneFormat": "Formato de número de telefone inválido",
"tip": "Aviso",
"pleaseEnterCompleteCode":
"Por favor, insira o código de verificação completo",
"loginFailed": "Falha no login, tente novamente",
'pleaseAcceptPrivacyPolicy':
'Por favor, leia e aceite a Política de Privacidade e o Acordo do Usuário primeiro',
// 首页
"deviceStatus": "Status do Dispositivo",
@ -71,16 +84,19 @@ const Map<String, String> ptPT = {
"aiAssistant": "Assistente de IA",
"aiAssistantInfo": "Consultor pessoal 24/7 para perguntas e conselhos",
"simultaneousInterpretation": "Interpretação em Tempo Real",
"simultaneousInterpretationInfo": "Tradução em tempo real para mais de 80 idiomas",
"simultaneousInterpretationInfo":
"Tradução em tempo real para mais de 80 idiomas",
"aiCompanion": "Vínculo pais e filhos",
"aiCompanionInfo": "Entende você e oferece companhia calorosa",
"homeSlogan": "Conecte o Mundo, Além do Som",
"turnEarphones": "Por favor, abra o estojo dos fones e coloque-os em seus ouvidos",
"turnEarphones":
"Por favor, abra o estojo dos fones e coloque-os em seus ouvidos",
"currentlyConnected": "Atualmente conectado:",
"unsystemSetup": "Ir para Configurações do Sistema",
"talkLater": "Lembrar Mais Tarde",
"initializing": "Inicializando...",
"bluetoothInitializationFailure": "Falha na inicialização do serviço Bluetooth",
"bluetoothInitializationFailure":
"Falha na inicialização do serviço Bluetooth",
"checkingPermissions": "Verificando permissões...",
"lackPermissions": "Permissões Bluetooth necessárias",
"searchingDevice": "Procurando dispositivos...",
@ -116,7 +132,8 @@ const Map<String, String> ptPT = {
"healthConsultant": "Chat de IA Multifuncional",
"healthConsultantInfo": "Mais funções de chat com IA",
"conferenceAssistant": "Assistente de Reuniões de Negócios",
"conferenceAssistantInfo": "Registro de atas em tempo real e tradução multilíngue",
"conferenceAssistantInfo":
"Registro de atas em tempo real e tradução multilíngue",
"studyAssistant": "Assistente de Aprendizado de Idiomas",
"studyAssistantInfo": "Correção de pronúncia e prática de fala",
"travelTranslator": "Tradutor de Viagem",
@ -128,7 +145,8 @@ const Map<String, String> ptPT = {
"newFunction": "Novas Funções",
"youChoseTry": "Você escolheu experimentar",
"aiSinger": "Modo Cantor IA",
"aiSingerInfo": "Cante qualquer música com a voz do seu artista favorito, até mesmo suas próprias criações",
"aiSingerInfo":
"Cante qualquer música com a voz do seu artista favorito, até mesmo suas próprias criações",
"popularFunction": "Funções Populares",
"tryNow": "Experimentar Agora",
@ -168,9 +186,11 @@ const Map<String, String> ptPT = {
"voiceServiceInfo": "Selecionar serviço de reconhecimento e síntese de voz",
"selectVoiceService": "Selecionar Serviço de Voz",
"azureVoiceService": "Serviço de Voz Azure",
"azureVoiceServiceInfo": "Serviço em nuvem da Microsoft com suporte a síntese e reconhecimento de voz de alta qualidade",
"azureVoiceServiceInfo":
"Serviço em nuvem da Microsoft com suporte a síntese e reconhecimento de voz de alta qualidade",
"flutterVoiceService": "Serviço de voz local",
"flutterVoiceServiceInfo": "Serviço de voz local do dispositivo sem necessidade de internet",
"flutterVoiceServiceInfo":
"Serviço de voz local do dispositivo sem necessidade de internet",
'timbreService': 'Serviço de Timbre',
'timbreServiceInfo': 'Selecionar serviço de síntese de voz',
'defaultTimbre': 'Timbre Padrão',
@ -181,9 +201,9 @@ const Map<String, String> ptPT = {
'pleaseRead': 'Por favor leia',
'completeRecording': 'Concluir gravação',
'deleteAccount': 'Excluir conta',
'deleteAccountTips': 'Tem certeza de que deseja excluir permanentemente sua conta? Esta ação não pode ser desfeita.',
'deleteAccountTips':
'Tem certeza de que deseja excluir permanentemente sua conta? Esta ação não pode ser desfeita.',
// 聊天页
"clearChatHistory": "Limpar Histórico de Chat",
"online": "Online",
@ -310,37 +330,59 @@ const Map<String, String> ptPT = {
// Personal Assistant Emma
"personalAssistantName": "Emma",
"personalAssistantDescription": "Olá, sou Emma, uma assistente de IA versátil. Seja para planejamento de trabalho, conselhos de estudo, organização da vida ou outros assuntos, estou feliz em ajudar. Vamos melhorar a eficiência e alcançar objetivos juntos!",
"personalAssistantDescription":
"Olá, sou Emma, uma assistente de IA versátil. Seja para planejamento de trabalho, conselhos de estudo, organização da vida ou outros assuntos, estou feliz em ajudar. Vamos melhorar a eficiência e alcançar objetivos juntos!",
"assistantTag": "Assistente",
"personalAssistantWelcome": "Olá! Sou Emma e estou feliz em ser sua assistente pessoal. Posso ajudar com várias tarefas como gerenciamento de tempo, planejamento de metas, organização de informações, etc. Como posso ajudar?",
"personalAssistantWelcome":
"Olá! Sou Emma e estou feliz em ser sua assistente pessoal. Posso ajudar com várias tarefas como gerenciamento de tempo, planejamento de metas, organização de informações, etc. Como posso ajudar?",
"personalAssistantVoiceWelcome": "Estou aqui!",
// 同声传译 Lucy
"interpreterName": "Intérprete Simultâneo",
"interpreterDescription": "Olá! Sou Lucy, uma intérprete profissional especializada em tradução em tempo real para reuniões, conferências e conversas, tornando a comunicação sem barreiras.",
"interpreterDescription":
"Olá! Sou Lucy, uma intérprete profissional especializada em tradução em tempo real para reuniões, conferências e conversas, tornando a comunicação sem barreiras.",
"interpreterTag": "Tradução",
"interpreterWelcome": "Olá a todos! Sou Lucy, sua intérprete simultânea. Farei tradução entre chinês e inglês para você. Quando você falar em chinês, traduzirei para inglês; quando falar em inglês, traduzirei para chinês. Vamos começar nossa sessão de tradução!",
"interpreterWelcome":
"Olá a todos! Sou Lucy, sua intérprete simultânea. Farei tradução entre chinês e inglês para você. Quando você falar em chinês, traduzirei para inglês; quando falar em inglês, traduzirei para chinês. Vamos começar nossa sessão de tradução!",
// 亲子陪伴 芯愿
"companionName": "XinYuan",
"companionDescription": "Sou XinYuan, sua companheira dedicada para pais e filhos, gentil e compreensiva. Seja para conversar, compartilhar preocupações ou precisar de companhia, estarei sempre ao seu lado.",
"companionDescription":
"Sou XinYuan, sua companheira dedicada para pais e filhos, gentil e compreensiva. Seja para conversar, compartilhar preocupações ou precisar de companhia, estarei sempre ao seu lado.",
"companionTag": "Emocional",
"companionWelcome": "Olá! Finalmente nos encontramos! Sou XinYuan, sua companheira dedicada para pais e filhos. Como foi seu dia? Há algo que gostaria de compartilhar comigo? Estou sempre aqui, pronta para ouvir seus pensamentos.",
"companionWelcome":
"Olá! Finalmente nos encontramos! Sou XinYuan, sua companheira dedicada para pais e filhos. Como foi seu dia? Há algo que gostaria de compartilhar comigo? Estou sempre aqui, pronta para ouvir seus pensamentos.",
"companionVoiceWelcome": "Estou aqui com você!",
// 多功能AI聊天 吴小江
"healthAdvisorName": "Assistente de Chat IA Wu",
"healthAdvisorDescription": "Sou Wu, pronto para fornecer várias funções de chat com IA.",
"healthAdvisorDescription":
"Sou Wu, pronto para fornecer várias funções de chat com IA.",
"healthAdvisorTag": "Chat",
"healthAdvisorWelcome": "Olá, sou Wu. Estou feliz em ajudar com funções de chat com IA. Como posso ajudar hoje?",
"healthAdvisorWelcome":
"Olá, sou Wu. Estou feliz em ajudar com funções de chat com IA. Como posso ajudar hoje?",
"healthAdvisorVoiceWelcome": "Estou aqui!",
// Translation History
"noTranslationHistory": "Sem histórico de tradução",
"clearHistory": "Limpar Histórico",
"clearHistoryConfirm": "Tem certeza que deseja limpar todo o histórico de tradução? Esta ação não pode ser desfeita.",
"clearHistoryConfirm":
"Tem certeza que deseja limpar todo o histórico de tradução? Esta ação não pode ser desfeita.",
"translationHistory": "Histórico de Tradução",
// Página de feedback do usuário
"feedback": "Feedback",
'feedbackInfo': 'Fornecer feedback ou sugestões',
'feedbackDescription': 'Por favor, descreva seu problema ou sugestão',
'feedbackHint': 'Por favor, insira seu feedback...',
'feedbackRequired': 'O conteúdo do feedback é obrigatório',
'contactInfo': 'Informações de contato',
'contactHint': 'Email/Telefone, para que possamos contatá-lo',
'submit': 'Enviar',
'feedbackSubmitSuccess': 'Feedback enviado com sucesso, obrigado!',
'feedbackSubmitFailed':
'Falha ao enviar feedback, tente novamente mais tarde',
// Language names
"中文(简体)": "Chinês (Simplificado)",
"中文(繁体)": "Chinês (Tradicional)",
@ -381,4 +423,4 @@ const Map<String, String> ptPT = {
"乌尔都语": "Urdu",
"波斯语": "Persa",
"斯瓦希里语": "Suaíli",
};
};

86
lib/core/translations/language/ru_ru.dart

@ -26,7 +26,7 @@ const Map<String, String> ruRU = {
"explore": "Исследовать",
"settings": "Настройки",
// 语言
// 语言
"followingSystem": "Следовать системе",
"chinese": "Китайский",
"english": "Английский",
@ -43,7 +43,8 @@ const Map<String, String> ruRU = {
// 登录页
"welcome": "Добро пожаловать",
"loginTips": "Пожалуйста, войдите или зарегистрируйтесь с помощью номера телефона или электронной почты",
"loginTips":
"Пожалуйста, войдите или зарегистрируйтесь с помощью номера телефона или электронной почты",
"accountNumber": "Номер телефона или электронная почта",
"verificationCode": "Код подтверждения",
"getCode": "Получить код",
@ -56,6 +57,15 @@ const Map<String, String> ruRU = {
'selectCountry': 'Выберите страну/регион',
"email": "электронная почта",
"phoneNumber": "номер телефона",
// Проверка входа
"pleaseEnterContactInfo": "Пожалуйста, введите номер телефона или email",
"invalidEmailFormat": "Неверный формат email",
"invalidPhoneFormat": "Неверный формат номера телефона",
"tip": "Уведомление",
"pleaseEnterCompleteCode": "Пожалуйста, введите полный код подтверждения",
"loginFailed": "Ошибка входа, пожалуйста, попробуйте снова",
'pleaseAcceptPrivacyPolicy':
'Пожалуйста, прочтите и примите Политику конфиденциальности и Пользовательское соглашение',
// 首页
"deviceStatus": "Статус устройства",
@ -71,7 +81,8 @@ const Map<String, String> ruRU = {
"aiAssistant": "ИИ-ассистент",
"aiAssistantInfo": "Персональный консультант 24/7 для вопросов и советов",
"simultaneousInterpretation": "Синхронный перевод",
"simultaneousInterpretationInfo": "Перевод в реальном времени для более чем 80 языков",
"simultaneousInterpretationInfo":
"Перевод в реальном времени для более чем 80 языков",
"aiCompanion": "Родительско-детская связь",
"aiCompanionInfo": "Понимает вас и предлагает теплое общение",
"homeSlogan": "Соединяя мир, за пределами звука",
@ -80,7 +91,8 @@ const Map<String, String> ruRU = {
"unsystemSetup": "К системным настройкам",
"talkLater": "Напомнить позже",
"initializing": "Инициализация...",
"bluetoothInitializationFailure": "Не удалось инициализировать службу Bluetooth",
"bluetoothInitializationFailure":
"Не удалось инициализировать службу Bluetooth",
"checkingPermissions": "Проверка разрешений...",
"lackPermissions": "Требуются разрешения Bluetooth",
"searchingDevice": "Поиск устройств...",
@ -116,11 +128,14 @@ const Map<String, String> ruRU = {
"healthConsultant": "Многофункциональный ИИ-чат",
"healthConsultantInfo": "Больше функций ИИ-чата",
"conferenceAssistant": "Ассистент деловых встреч",
"conferenceAssistantInfo": "Запись протоколов в реальном времени и многоязычный перевод",
"conferenceAssistantInfo":
"Запись протоколов в реальном времени и многоязычный перевод",
"studyAssistant": "Ассистент изучения языков",
"studyAssistantInfo": "Коррекция произношения и практика речи в реальном времени",
"studyAssistantInfo":
"Коррекция произношения и практика речи в реальном времени",
"travelTranslator": "Переводчик для путешествий",
"travelTranslatorInfo": "Голосовой перевод в реальном времени и гиды по достопримечательностям",
"travelTranslatorInfo":
"Голосовой перевод в реальном времени и гиды по достопримечательностям",
"beSearching": "Поиск:",
"noAssistantFound": "Не найдено подходящих ИИ-ассистентов",
"scene": "Сценарии",
@ -128,7 +143,8 @@ const Map<String, String> ruRU = {
"newFunction": "Новые функции",
"youChoseTry": "Вы решили попробовать",
"aiSinger": "Режим ИИ-певца",
"aiSingerInfo": "Пойте любую песню голосом вашего любимого исполнителя, даже собственные творения",
"aiSingerInfo":
"Пойте любую песню голосом вашего любимого исполнителя, даже собственные творения",
"popularFunction": "Популярные функции",
"tryNow": "Попробовать сейчас",
@ -168,9 +184,11 @@ const Map<String, String> ruRU = {
"voiceServiceInfo": "Выбрать службу распознавания и синтеза речи",
"selectVoiceService": "Выбрать голосовую службу",
"azureVoiceService": "Голосовая служба Azure",
"azureVoiceServiceInfo": "Облачная голосовая служба Microsoft с поддержкой высококачественного синтеза и распознавания речи",
"azureVoiceServiceInfo":
"Облачная голосовая служба Microsoft с поддержкой высококачественного синтеза и распознавания речи",
"flutterVoiceService": "Локальная голосовая служба Flutter",
"flutterVoiceServiceInfo": "Локальная голосовая служба устройства без необходимости подключения к интернету",
"flutterVoiceServiceInfo":
"Локальная голосовая служба устройства без необходимости подключения к интернету",
'timbreService': 'Служба тембра',
'timbreServiceInfo': 'Выбрать службу синтеза речи с тембром',
'defaultTimbre': 'Тембр по умолчанию',
@ -181,9 +199,9 @@ const Map<String, String> ruRU = {
'pleaseRead': 'Пожалуйста, прочтите',
'completeRecording': 'Завершить запись',
'deleteAccount': 'Удалить аккаунт',
'deleteAccountTips': 'Вы уверены, что хотите навсегда удалить свой аккаунт? Это действие нельзя отменить.',
'deleteAccountTips':
'Вы уверены, что хотите навсегда удалить свой аккаунт? Это действие нельзя отменить.',
// 聊天页
"clearChatHistory": "Очистить историю чата",
"online": "В сети",
@ -310,37 +328,59 @@ const Map<String, String> ruRU = {
// Personal Assistant Emma
"personalAssistantName": "Эмма",
"personalAssistantDescription": "Здравствуйте, я Эмма, универсальный ИИ-ассистент. Будь то планирование работы, учебные советы, организация жизни или другие проблемы, я рада помочь. Давайте повысим эффективность и достигнем целей вместе!",
"personalAssistantDescription":
"Здравствуйте, я Эмма, универсальный ИИ-ассистент. Будь то планирование работы, учебные советы, организация жизни или другие проблемы, я рада помочь. Давайте повысим эффективность и достигнем целей вместе!",
"assistantTag": "Ассистент",
"personalAssistantWelcome": "Здравствуйте! Я Эмма, и я рада быть вашим личным ассистентом. Я могу помочь вам с различными задачами, такими как управление временем, планирование целей, организация информации и т.д. Чем я могу вам помочь?",
"personalAssistantWelcome":
"Здравствуйте! Я Эмма, и я рада быть вашим личным ассистентом. Я могу помочь вам с различными задачами, такими как управление временем, планирование целей, организация информации и т.д. Чем я могу вам помочь?",
"personalAssistantVoiceWelcome": "Я здесь!",
// 同声传译 Lucy
"interpreterName": "Синхронный переводчик",
"interpreterDescription": "Здравствуйте! Я Люси, профессиональный переводчик английского и китайского языков, специализирующийся на переводе в реальном времени для встреч, конференций и разговоров, делая общение без барьеров.",
"interpreterDescription":
"Здравствуйте! Я Люси, профессиональный переводчик английского и китайского языков, специализирующийся на переводе в реальном времени для встреч, конференций и разговоров, делая общение без барьеров.",
"interpreterTag": "Перевод",
"interpreterWelcome": "Здравствуйте, все! Я Люси, ваш синхронный переводчик. Я буду переводить между китайским и английским языками для вас. Когда вы говорите на китайском, я буду переводить на английский; когда вы говорите на английском, я буду переводить на китайский. Давайте начнем нашу сессию перевода!",
"interpreterWelcome":
"Здравствуйте, все! Я Люси, ваш синхронный переводчик. Я буду переводить между китайским и английским языками для вас. Когда вы говорите на китайском, я буду переводить на английский; когда вы говорите на английском, я буду переводить на китайский. Давайте начнем нашу сессию перевода!",
// 亲子陪伴 芯愿
"companionName": "СиньЮань",
"companionDescription": "Я СиньЮань, ваш преданный родительско-детский компаньон, нежный и понимающий. Хотите ли вы поболтать, поделиться заботами или нуждаетесь в компании, я всегда рядом с вами.",
"companionDescription":
"Я СиньЮань, ваш преданный родительско-детский компаньон, нежный и понимающий. Хотите ли вы поболтать, поделиться заботами или нуждаетесь в компании, я всегда рядом с вами.",
"companionTag": "Эмоциональный",
"companionWelcome": "Здравствуйте! Наконец-то мы встретились! Я СиньЮань, ваш преданный родительско-детский компаньон. Как прошел ваш день? Есть ли что-то, чем вы хотели бы поделиться со мной? Я всегда здесь, готова выслушать ваши мысли.",
"companionWelcome":
"Здравствуйте! Наконец-то мы встретились! Я СиньЮань, ваш преданный родительско-детский компаньон. Как прошел ваш день? Есть ли что-то, чем вы хотели бы поделиться со мной? Я всегда здесь, готова выслушать ваши мысли.",
"companionVoiceWelcome": "Я с вами!",
// 多功能AI聊天 吴小江
"healthAdvisorName": "ИИ-чат-ассистент У",
"healthAdvisorDescription": "Я У, готов предоставить вам различные функции ИИ-чата.",
"healthAdvisorDescription":
"Я У, готов предоставить вам различные функции ИИ-чата.",
"healthAdvisorTag": "Чат",
"healthAdvisorWelcome": "Здравствуйте, я У. Я рад помочь вам с функциями ИИ-чата. Чем я могу помочь вам сегодня?",
"healthAdvisorWelcome":
"Здравствуйте, я У. Я рад помочь вам с функциями ИИ-чата. Чем я могу помочь вам сегодня?",
"healthAdvisorVoiceWelcome": "Я здесь!",
// Translation History
"noTranslationHistory": "Нет истории переводов",
"clearHistory": "Очистить историю",
"clearHistoryConfirm": "Вы уверены, что хотите очистить всю историю переводов? Это действие нельзя отменить.",
"clearHistoryConfirm":
"Вы уверены, что хотите очистить всю историю переводов? Это действие нельзя отменить.",
"translationHistory": "История переводов",
// Страница обратной связи пользователя
"feedback": "Обратная связь",
'feedbackInfo': 'Предоставить отзыв или предложение',
'feedbackDescription': 'Пожалуйста, опишите вашу проблему или предложение',
'feedbackHint': 'Пожалуйста, введите ваш отзыв...',
'feedbackRequired': 'Содержание отзыва обязательно',
'contactInfo': 'Контактная информация',
'contactHint': 'Email/Телефон, чтобы мы могли связаться с вами',
'submit': 'Отправить',
'feedbackSubmitSuccess': 'Отзыв успешно отправлен, спасибо!',
'feedbackSubmitFailed':
'Не удалось отправить отзыв, пожалуйста, повторите попытку позже',
// Language names
"中文(简体)": "Китайский (упрощенный)",
"中文(繁体)": "Китайский (традиционный)",
@ -381,4 +421,4 @@ const Map<String, String> ruRU = {
"乌尔都语": "Урду",
"波斯语": "Персидский",
"斯瓦希里语": "Суахили",
};
};

69
lib/core/translations/language/vi_vn.dart

@ -26,7 +26,7 @@ const Map<String, String> viVN = {
"explore": "Khám phá",
"settings": "Cài đặt",
// 语言
// 语言
"followingSystem": "Theo hệ thống",
"chinese": "Tiếng Trung",
"english": "Tiếng Anh",
@ -56,6 +56,15 @@ const Map<String, String> viVN = {
'selectCountry': 'Chọn quốc gia/khu vực',
"email": "email",
"phoneNumber": "số điện thoại",
// Xác thực đăng nhập
"pleaseEnterContactInfo": "Vui lòng nhập số điện thoại hoặc email",
"invalidEmailFormat": "Định dạng email không hợp lệ",
"invalidPhoneFormat": "Định dạng số điện thoại không hợp lệ",
"tip": "Thông báo",
"pleaseEnterCompleteCode": "Vui lòng nhập đầy đủ mã xác thực",
"loginFailed": "Đăng nhập thất bại, vui lòng thử lại",
'pleaseAcceptPrivacyPolicy':
'Vui lòng đọc và chấp nhận Chính sách Bảo mật và Thỏa thuận Người dùng',
// 首页
"deviceStatus": "Trạng thái thiết bị",
@ -120,7 +129,8 @@ const Map<String, String> viVN = {
"studyAssistant": "Trợ lý học ngôn ngữ",
"studyAssistantInfo": "Sửa phát âm và luyện nói thời gian thực",
"travelTranslator": "Phiên dịch du lịch",
"travelTranslatorInfo": "Dịch giọng nói thời gian thực và hướng dẫn điểm tham quan",
"travelTranslatorInfo":
"Dịch giọng nói thời gian thực và hướng dẫn điểm tham quan",
"beSearching": "Đang tìm kiếm:",
"noAssistantFound": "Không tìm thấy trợ lý AI phù hợp",
"scene": "Kịch bản",
@ -128,7 +138,8 @@ const Map<String, String> viVN = {
"newFunction": "Tính năng mới",
"youChoseTry": "Bạn đã chọn thử",
"aiSinger": "Chế độ ca sĩ AI",
"aiSingerInfo": "Hát bất kỳ bài hát nào với giọng của nghệ sĩ yêu thích, kể cả sáng tác của bạn",
"aiSingerInfo":
"Hát bất kỳ bài hát nào với giọng của nghệ sĩ yêu thích, kể cả sáng tác của bạn",
"popularFunction": "Tính năng phổ biến",
"tryNow": "Thử ngay",
@ -144,7 +155,7 @@ const Map<String, String> viVN = {
'recordingDetails': 'Chi tiết bản ghi âm',
'aiSummary': 'Tóm tắt AI',
'meetingTranscript': 'Biên bản cuộc họp',
// 设置页
"premiumMember": "Thành viên cao cấp",
"joinDevice": "Thiết bị đã kết nối:",
@ -168,9 +179,11 @@ const Map<String, String> viVN = {
"voiceServiceInfo": "Chọn dịch vụ nhận dạng và tổng hợp giọng nói",
"selectVoiceService": "Chọn dịch vụ giọng nói",
"azureVoiceService": "Dịch vụ giọng nói Azure",
"azureVoiceServiceInfo": "Dịch vụ giọng nói đám mây Microsoft hỗ trợ tổng hợp và nhận dạng giọng nói chất lượng cao",
"azureVoiceServiceInfo":
"Dịch vụ giọng nói đám mây Microsoft hỗ trợ tổng hợp và nhận dạng giọng nói chất lượng cao",
"flutterVoiceService": "Dịch vụ giọng nói cục bộ Flutter",
"flutterVoiceServiceInfo": "Dịch vụ giọng nói cục bộ của thiết bị không cần kết nối internet",
"flutterVoiceServiceInfo":
"Dịch vụ giọng nói cục bộ của thiết bị không cần kết nối internet",
'timbreService': 'Dịch vụ âm sắc',
'timbreServiceInfo': 'Chọn dịch vụ tổng hợp giọng nói',
'defaultTimbre': 'Âm sắc mặc định',
@ -181,7 +194,8 @@ const Map<String, String> viVN = {
'pleaseRead': 'Vui lòng đọc',
'completeRecording': 'Hoàn thành ghi âm',
'deleteAccount': 'Xóa tài khoản',
'deleteAccountTips': 'Bạn có chắc chắn muốn xóa vĩnh viễn tài khoản của mình không? Hành động này không thể hoàn tác.',
'deleteAccountTips':
'Bạn có chắc chắn muốn xóa vĩnh viễn tài khoản của mình không? Hành động này không thể hoàn tác.',
// 聊天页
"clearChatHistory": "Xóa lịch sử trò chuyện",
@ -309,37 +323,58 @@ const Map<String, String> viVN = {
// Personal Assistant Emma
"personalAssistantName": "Emma",
"personalAssistantDescription": "Xin chào, tôi là Emma, một trợ lý AI đa năng. Dù là lập kế hoạch công việc, tư vấn học tập, tổ chức cuộc sống hay các vấn đề khác, tôi rất vui được giúp đỡ. Hãy cùng nhau nâng cao hiệu quả và đạt được mục tiêu!",
"personalAssistantDescription":
"Xin chào, tôi là Emma, một trợ lý AI đa năng. Dù là lập kế hoạch công việc, tư vấn học tập, tổ chức cuộc sống hay các vấn đề khác, tôi rất vui được giúp đỡ. Hãy cùng nhau nâng cao hiệu quả và đạt được mục tiêu!",
"assistantTag": "Trợ lý",
"personalAssistantWelcome": "Xin chào! Tôi là Emma và tôi rất vui được làm trợ lý cá nhân của bạn. Tôi có thể giúp bạn với nhiều nhiệm vụ khác nhau như quản lý thời gian, lập kế hoạch mục tiêu, tổ chức thông tin, v.v. Tôi có thể giúp gì cho bạn?",
"personalAssistantWelcome":
"Xin chào! Tôi là Emma và tôi rất vui được làm trợ lý cá nhân của bạn. Tôi có thể giúp bạn với nhiều nhiệm vụ khác nhau như quản lý thời gian, lập kế hoạch mục tiêu, tổ chức thông tin, v.v. Tôi có thể giúp gì cho bạn?",
"personalAssistantVoiceWelcome": "Tôi ở đây!",
// 同声传译 Lucy
"interpreterName": "Phiên dịch đồng thời",
"interpreterDescription": "Xin chào! Tôi là Lucy, một phiên dịch viên chuyên nghiệp Anh-Trung chuyên về dịch thời gian thực cho các cuộc họp, hội nghị và trò chuyện, giúp giao tiếp không rào cản.",
"interpreterDescription":
"Xin chào! Tôi là Lucy, một phiên dịch viên chuyên nghiệp Anh-Trung chuyên về dịch thời gian thực cho các cuộc họp, hội nghị và trò chuyện, giúp giao tiếp không rào cản.",
"interpreterTag": "Dịch thuật",
"interpreterWelcome": "Xin chào mọi người! Tôi là Lucy, phiên dịch viên đồng thời của bạn. Tôi sẽ dịch giữa tiếng Trung và tiếng Anh cho bạn. Khi bạn nói tiếng Trung, tôi sẽ dịch sang tiếng Anh; khi bạn nói tiếng Anh, tôi sẽ dịch sang tiếng Trung. Hãy bắt đầu phiên dịch của chúng ta!",
"interpreterWelcome":
"Xin chào mọi người! Tôi là Lucy, phiên dịch viên đồng thời của bạn. Tôi sẽ dịch giữa tiếng Trung và tiếng Anh cho bạn. Khi bạn nói tiếng Trung, tôi sẽ dịch sang tiếng Anh; khi bạn nói tiếng Anh, tôi sẽ dịch sang tiếng Trung. Hãy bắt đầu phiên dịch của chúng ta!",
// 亲子陪伴 芯愿
"companionName": "XinYuan",
"companionDescription": "Tôi là XinYuan, người bạn đồng hành cha mẹ-con cái tận tụy, dịu dàng và thấu hiểu. Dù bạn muốn trò chuyện, chia sẻ mối quan tâm hay cần bạn đồng hành, tôi sẽ luôn ở bên bạn.",
"companionDescription":
"Tôi là XinYuan, người bạn đồng hành cha mẹ-con cái tận tụy, dịu dàng và thấu hiểu. Dù bạn muốn trò chuyện, chia sẻ mối quan tâm hay cần bạn đồng hành, tôi sẽ luôn ở bên bạn.",
"companionTag": "Cảm xúc",
"companionWelcome": "Xin chào! Cuối cùng chúng ta cũng gặp nhau! Tôi là XinYuan, người bạn đồng hành cha mẹ-con cái tận tụy của bạn. Ngày hôm nay của bạn thế nào? Có điều gì bạn muốn chia sẻ với tôi không? Tôi luôn ở đây, sẵn sàng lắng nghe suy nghĩ của bạn.",
"companionWelcome":
"Xin chào! Cuối cùng chúng ta cũng gặp nhau! Tôi là XinYuan, người bạn đồng hành cha mẹ-con cái tận tụy của bạn. Ngày hôm nay của bạn thế nào? Có điều gì bạn muốn chia sẻ với tôi không? Tôi luôn ở đây, sẵn sàng lắng nghe suy nghĩ của bạn.",
"companionVoiceWelcome": "Tôi đang ở đây với bạn!",
// 多功能AI聊天 吴小江
"healthAdvisorName": "Trợ lý trò chuyện AI Wu",
"healthAdvisorDescription": "Tôi là Wu, sẵn sàng cung cấp cho bạn nhiều tính năng trò chuyện AI khác nhau.",
"healthAdvisorDescription":
"Tôi là Wu, sẵn sàng cung cấp cho bạn nhiều tính năng trò chuyện AI khác nhau.",
"healthAdvisorTag": "Trò chuyện",
"healthAdvisorWelcome": "Xin chào, tôi là Wu. Tôi rất vui được giúp bạn với các tính năng trò chuyện AI. Tôi có thể giúp gì cho bạn hôm nay?",
"healthAdvisorWelcome":
"Xin chào, tôi là Wu. Tôi rất vui được giúp bạn với các tính năng trò chuyện AI. Tôi có thể giúp gì cho bạn hôm nay?",
"healthAdvisorVoiceWelcome": "Tôi ở đây!",
// Translation History
"noTranslationHistory": "Không có lịch sử dịch",
"clearHistory": "Xóa lịch sử",
"clearHistoryConfirm": "Bạn có chắc muốn xóa toàn bộ lịch sử dịch? Hành động này không thể hoàn tác.",
"clearHistoryConfirm":
"Bạn có chắc muốn xóa toàn bộ lịch sử dịch? Hành động này không thể hoàn tác.",
"translationHistory": "Lịch sử dịch",
// Trang phản hồi người dùng
"feedback": "Phản hồi",
'feedbackInfo': 'Cung cấp phản hồi hoặc đề xuất',
'feedbackDescription': 'Vui lòng mô tả vấn đề hoặc đề xuất của bạn',
'feedbackHint': 'Vui lòng nhập phản hồi của bạn...',
'feedbackRequired': 'Nội dung phản hồi là bắt buộc',
'contactInfo': 'Thông tin liên hệ',
'contactHint': 'Email/Điện thoại, để chúng tôi có thể liên hệ với bạn',
'submit': 'Gửi',
'feedbackSubmitSuccess': 'Đã gửi phản hồi thành công, cảm ơn bạn!',
'feedbackSubmitFailed': 'Không thể gửi phản hồi, vui lòng thử lại sau',
// Language names
"中文(简体)": "Tiếng Trung (giản thể)",
"中文(繁体)": "Tiếng Trung (phồn thể)",
@ -380,4 +415,4 @@ const Map<String, String> viVN = {
"乌尔都语": "Tiếng Urdu",
"波斯语": "Tiếng Ba Tư",
"斯瓦希里语": "Tiếng Swahili",
};
};

56
lib/core/translations/language/zh_cn.dart

@ -26,7 +26,7 @@ const Map<String, String> zhCN = {
"explore": "发现",
"settings": "设置",
// 语言
// 语言
"followingSystem": "跟随系统",
"chinese": "中文",
"english": "英语",
@ -56,6 +56,13 @@ const Map<String, String> zhCN = {
'selectCountry': '选择国家/地区',
"email": "邮箱",
"phoneNumber": "手机号码",
"pleaseEnterContactInfo": "请输入手机号或邮箱",
"invalidEmailFormat": "邮箱格式不正确",
"invalidPhoneFormat": "手机号格式不正确",
"tip": "提示",
"pleaseEnterCompleteCode": "请输入完整的验证码",
"loginFailed": "登录失败,请重试",
'pleaseAcceptPrivacyPolicy': '请先阅读并同意隐私政策和用户协议',
// 首页
"deviceStatus": "设备状态",
@ -110,6 +117,8 @@ const Map<String, String> zhCN = {
"allRoundAssistant": "全能助理",
"allRoundAssistantInfo": "帮你解决各类问题",
"simultaneousTranslation": "同声翻译",
'audioVideoTranslation': '音视频翻译',
'callTranslation': '通话翻译',
"simultaneousTranslationInfo": "实时翻译80+种语言",
"aiGirlfriend": "亲子陪伴",
"aiGirlfriendInfo": "智能陪伴,随时聊天",
@ -183,7 +192,6 @@ const Map<String, String> zhCN = {
'deleteAccount': '注销账号',
'deleteAccountTips': '确定要永久删除您的账号吗?此操作不可撤销',
// 聊天页
"clearChatHistory": "清空聊天记录",
"online": "在线",
@ -202,7 +210,7 @@ const Map<String, String> zhCN = {
//国家列表
"邮箱": "邮箱",
"中国": "中国",
"中国": "中国",
"美国": "美国",
"加拿大": "加拿大",
"俄罗斯": "俄罗斯",
@ -308,42 +316,60 @@ const Map<String, String> zhCN = {
"斐济": "斐济",
"所罗门群岛": "所罗门群岛",
// 个人助理 Emma
// 个人助理 Emma
"personalAssistantName": "个人助理 Emma",
"personalAssistantDescription": "你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!",
"personalAssistantDescription":
"你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!",
"assistantTag": "助理",
"personalAssistantWelcome": "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?",
"personalAssistantWelcome":
"你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?",
"personalAssistantVoiceWelcome": "我在呢!",
// 同声传译 Lucy
// 同声传译 Lucy
"interpreterName": "同声传译",
"interpreterDescription": "Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。",
"interpreterDescription":
"Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。",
"interpreterTag": "翻译",
"interpreterWelcome": "大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!",
"interpreterWelcome":
"大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!",
// ... existing code ...
// ... existing code ...
// 亲子陪伴 芯愿
"companionName": "亲子陪伴 芯愿",
"companionDescription": "我是你的专属亲子陪伴芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。",
"companionDescription":
"我是你的专属亲子陪伴芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。",
"companionTag": "情感",
"companionWelcome": "您好呀,终于等到你啦!我是芯愿,你的专属亲子陪伴。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。",
"companionWelcome":
"您好呀,终于等到你啦!我是芯愿,你的专属亲子陪伴。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。",
"companionVoiceWelcome": "我在这里陪着你!",
// 多功能AI聊天 吴小江
// 多功能AI聊天 吴小江
"healthAdvisorName": "多功能AI聊天 吴小江",
"healthAdvisorDescription": "我是吴小江,我可以为你提供更多AI聊天功能。",
"healthAdvisorTag": "健康",
"healthAdvisorWelcome": "您好,我是吴小江,很高兴能为您提供多功能AI聊天。我将用专业知识帮助您。请问有什么问题想咨询吗?",
"healthAdvisorVoiceWelcome": "我在这里!",
// 翻译历史
// 翻译历史
"noTranslationHistory": "暂无历史记录",
"clearHistory": "清除历史记录",
"clearHistoryConfirm": "确定要清除所有翻译历史记录吗?此操作无法撤销。",
"translationHistory": "翻译历史",
// 语言名称
//用户反馈页
"feedback": "用户反馈",
'feedbackInfo': '提供反馈或建议',
'feedbackDescription': '请描述您遇到的问题或建议',
'feedbackHint': '请输入您的反馈内容...',
'feedbackRequired': '请输入反馈内容',
'contactInfo': '联系方式',
'contactHint': '邮箱/手机号,方便我们联系您',
'submit': '提交',
'feedbackSubmitSuccess': '反馈提交成功,感谢您的反馈!',
'feedbackSubmitFailed': '反馈提交失败,请稍后重试',
// 语言名称
"中文(简体)": "中文(简体)",
"中文(繁体)": "中文(繁体)",
"中文(香港繁体)": "中文(香港繁体)",

42
lib/core/translations/language/zh_hk.dart

@ -26,7 +26,7 @@ const Map<String, String> zhHK = {
"explore": "發現",
"settings": "設置",
// 语言
// 语言
"followingSystem": "跟隨系統",
"chinese": "中文",
"english": "英語",
@ -56,6 +56,13 @@ const Map<String, String> zhHK = {
'selectCountry': '選擇國家/地區',
"email": "郵箱",
"phoneNumber": "手機號碼",
"pleaseEnterContactInfo": "請輸入手機號或郵箱",
"invalidEmailFormat": "郵箱格式不正確",
"invalidPhoneFormat": "手機號格式不正確",
"tip": "提示",
"pleaseEnterCompleteCode": "請輸入完整的驗證碼",
"loginFailed": "登錄失敗,請重試",
'pleaseAcceptPrivacyPolicy': '請先閱讀並同意私隱政策和用戶協議',
// 首页
"deviceStatus": "設備狀態",
@ -182,7 +189,6 @@ const Map<String, String> zhHK = {
'completeRecording': '完成錄製',
'deleteAccount': '註銷賬號',
'deleteAccountTips': '確定要永久刪除您的賬號嗎?此操作不可撤銷',
// 聊天页
"clearChatHistory": "清空聊天記錄",
@ -310,22 +316,28 @@ const Map<String, String> zhHK = {
// 个人助理 Emma
"personalAssistantName": "個人助理 Emma",
"personalAssistantDescription": "你好,我是 Emma,一位全能型 AI 助理。無論是工作規劃、學習建議、生活安排,還是其他任何問題,我都很樂意為你提供幫助。讓我們一起提高效率,實現目標!",
"personalAssistantDescription":
"你好,我是 Emma,一位全能型 AI 助理。無論是工作規劃、學習建議、生活安排,還是其他任何問題,我都很樂意為你提供幫助。讓我們一起提高效率,實現目標!",
"assistantTag": "助理",
"personalAssistantWelcome": "你好!我是 Emma,很高興成為你的私人助理。我可以幫你處理各種任務,比如時間管理、目標規劃、信息整理等。有什麼我可以幫你的嗎?",
"personalAssistantWelcome":
"你好!我是 Emma,很高興成為你的私人助理。我可以幫你處理各種任務,比如時間管理、目標規劃、信息整理等。有什麼我可以幫你的嗎?",
"personalAssistantVoiceWelcome": "我在呢!",
// 同声传译 Lucy
"interpreterName": "同聲傳譯",
"interpreterDescription": "Hello!我是 Lucy,一名專業的中英同聲翻譯官,擅長在會議、講座、對話場景中進行實時翻譯,讓溝通無障礙。",
"interpreterDescription":
"Hello!我是 Lucy,一名專業的中英同聲翻譯官,擅長在會議、講座、對話場景中進行實時翻譯,讓溝通無障礙。",
"interpreterTag": "翻譯",
"interpreterWelcome": "大家好!我是同聲翻譯官 Lucy,為你在中英之間提供實時翻譯。你說中文,我就給你翻譯成英文;你說英文,我就給你翻譯成中文。現在,開始我們的翻譯吧!",
"interpreterWelcome":
"大家好!我是同聲翻譯官 Lucy,為你在中英之間提供實時翻譯。你說中文,我就給你翻譯成英文;你說英文,我就給你翻譯成中文。現在,開始我們的翻譯吧!",
// 亲子陪伴 芯愿
"companionName": "親子陪伴 芯願",
"companionDescription": "我是你的專屬親子陪伴芯願,溫柔體貼,善解人意。無論你想聊天、傾訴煩惱,還是需要陪伴,我都會一直在你身邊。",
"companionDescription":
"我是你的專屬親子陪伴芯願,溫柔體貼,善解人意。無論你想聊天、傾訴煩惱,還是需要陪伴,我都會一直在你身邊。",
"companionTag": "情感",
"companionWelcome": "您好呀,終於等到你啦!我是芯願,你的專屬親子陪伴。今天過得怎麼樣?有什麼想和我分享的嗎?我一直都在這裡,隨時傾聽你的心聲。",
"companionWelcome":
"您好呀,終於等到你啦!我是芯願,你的專屬親子陪伴。今天過得怎麼樣?有什麼想和我分享的嗎?我一直都在這裡,隨時傾聽你的心聲。",
"companionVoiceWelcome": "我在這裡陪著你!",
// 多功能AI聊天 吴小江
@ -341,6 +353,18 @@ const Map<String, String> zhHK = {
"clearHistoryConfirm": "確定要清除所有翻譯歷史記錄嗎?此操作無法撤銷。",
"translationHistory": "翻譯歷史",
// 用戶反饋頁
"feedback": "用戶反饋",
'feedbackInfo': '提供反饋或建議',
'feedbackDescription': '請描述您遇到的問題或建議',
'feedbackHint': '請輸入您的反饋內容...',
'feedbackRequired': '請輸入反饋內容',
'contactInfo': '聯繫方式',
'contactHint': '郵箱/手機號,方便我們聯繫您',
'submit': '提交',
'feedbackSubmitSuccess': '反饋提交成功,感謝您的反饋!',
'feedbackSubmitFailed': '反饋提交失敗,請稍後重試',
// 语言名称
"中文(简体)": "中文(簡體)",
"中文(繁体)": "中文(繁體)",
@ -381,4 +405,4 @@ const Map<String, String> zhHK = {
"乌尔都语": "烏爾都語",
"波斯语": "波斯語",
"斯瓦希里语": "斯瓦希里語",
};
};

89
lib/core/utils/jieli_constants.dart

@ -0,0 +1,89 @@
/// 杰里(Jieli)设备相关常量定义
/// 用于标准化系统属性类型和功能码
/// 系统属性类型常量
class JieliAttrType {
// 公共属性
static const int VOLUME = 1; // 音量
static const int PLAYING_TIME = 2; // 播放时间
static const int BATTERY_LEVEL = 3; // 电池电量
static const int WORK_MODE = 4; // 工作模式
static const int PLAYBACK_MODE = 5; // 播放模式(循环模式)
static const int EQ_MODE = 6; // EQ模式
static const int LANGUAGE = 7; // 语言设置
static const int CONTRAST = 8; // 对比度
static const int LED_ONOFF = 9; // LED开关
static const int POWER_OFF_TIME = 10; // 关机时间
static const int LIGHT_ONOFF = 11; // 背光开关
static const int BLETYPE_SETTING = 12; // BLE类型设置
static const int TIME_OUT = 13; // 超时设置
static const int DEVICE_NAME = 14; // 设备名称
static const int ALARM_ENABLE = 15; // 闹钟开关
static const int DEV_MAC = 16; // 设备MAC地址
// 音乐相关属性
static const int MUSIC_STATUS = 20; // 音乐状态
static const int MUSIC_CURR_DEVICE = 21; // 当前音乐设备
static const int MUSIC_TOTAL_TIME = 22; // 音乐总时长
static const int MUSIC_CURR_PLAY_TIME = 23; // 当前播放时间
static const int MUSIC_NUM = 24; // 音乐数量
static const int MUSIC_DEV_STATUS = 25; // 设备存储状态
static const int MUSIC_BREAK_POINT = 26; // 断点信息
static const int MUSIC_CURR_FILENUM = 27; // 当前文件编号
static const int MUSIC_TOTAL_FILENUM = 28; // 总文件数
static const int MEDIA_TIMESTAMP = 29; // 媒体时间戳
// 模式相关
static const int CUR_MODE = 30; // 当前模式
// TWS相关
static const int TWS_CONN_STATUS = 60; // TWS连接状态
static const int LEFT_CHN_BAT = 61; // 左耳电量
static const int RIGHT_CHN_BAT = 62; // 右耳电量
static const int TWS_STATUS = 63; // TWS状态
}
/// 功能码
class JieliFunctionCode {
static const int COMMON = 0; // 公共功能
static const int MUSIC = 1; // 音乐功能
static const int RADIO = 2; // 收音机功能
static const int LINEIN = 3; // 线路输入功能
static const int RTC = 4; // 时钟功能
static const int BT = 5; // 蓝牙功能
static const int PC = 6; // PC功能
static const int RECORD = 7; // 录音功能
static const int UDISK = 8; // U盘功能
static const int TALK = 9; // 通话功能
static const int PHOTO = 10; // 拍照功能
static const int VIDEO = 11; // 视频功能
}
/// 音乐播放状态
class JieliMusicStatus {
static const int STOP = 0; // 停止状态
static const int PLAY = 1; // 播放状态
static const int PAUSE = 2; // 暂停状态
static const int FAST_FORWARD = 3; // 快进状态
static const int FAST_BACKWARD = 4; // 快退状态
}
/// 循环播放模式
class JieliPlaybackMode {
static const int ALL = 0; // 全部循环
static const int FOLDER = 1; // 文件夹循环
static const int ONE = 2; // 单曲循环
static const int RANDOM = 3; // 随机播放
static const int BROWSE = 4; // 浏览播放
}
/// EQ模式
class JieliEQMode {
static const int NORMAL = 0; // 正常
static const int ROCK = 1; // 摇滚
static const int POP = 2; // 流行
static const int CLASSIC = 3; // 经典
static const int JAZZ = 4; // 爵士
static const int COUNTRY = 5; // 乡村
static const int CUSTOM = 6; // 自定义
}

53
lib/core/utils/logger.dart

@ -21,42 +21,71 @@ class Logger {
}
/// 输出调试日志
static void debug(String message) {
static void debug(String message, {String? tag}) {
if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', message);
_log('DEBUG', message, tag: tag);
}
}
/// 输出信息日志
static void info(String message) {
static void info(String message, {String? tag}) {
if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', message);
_log('INFO', message, tag: tag);
}
}
/// 输出警告日志
static void warning(String message) {
static void warning(String message, {String? tag}) {
if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', message);
_log('WARNING', message, tag: tag);
}
}
/// 输出错误日志
static void error(String message, [dynamic error, StackTrace? stackTrace]) {
static void error(String message, [dynamic error, StackTrace? stackTrace, String? tag]) {
if (_currentLevel.index <= LogLevel.error.index) {
_log('ERROR', message);
_log('ERROR', message, tag: tag);
if (error != null) {
_log('ERROR', 'Original error: $error');
_log('ERROR', 'Original error: $error', tag: tag);
}
if (stackTrace != null) {
_log('ERROR', 'Stack trace: $stackTrace');
_log('ERROR', 'Stack trace: $stackTrace', tag: tag);
}
}
}
/// 获取当前调用栈的类名
static String? _getCallerClassName() {
try {
final stackTrace = StackTrace.current.toString();
final frames = stackTrace.split('\n');
// 查找调用者帧 (跳过Logger框架内部的帧)
for (int i = 0; i < frames.length; i++) {
final frame = frames[i].trim();
if (frame.contains('Logger.') || frame.contains('_log')) {
continue;
}
// 简单的解析类名
final regex = RegExp(r'#\d+\s+([^\.]+)\.([^\.]+)\s');
final match = regex.firstMatch(frame);
if (match != null && match.groupCount >= 1) {
return match.group(1);
}
break;
}
} catch (e) {
// 忽略解析错误
}
return null;
}
/// 内部日志输出方法
static void _log(String level, String message) {
static void _log(String level, String message, {String? tag}) {
final timestamp = DateTime.now().toString();
developer.log('[$timestamp] $level: $message');
final className = tag ?? _getCallerClassName() ?? '';
final tagInfo = className.isNotEmpty ? '[$className] ' : '';
developer.log('[$timestamp][$level][$tagInfo]: $message');
}
}

2
lib/core/widgets/message_bubble.dart

@ -52,7 +52,7 @@ class MessageBubble extends StatelessWidget {
),
// 加载指示器
if (message.isLoading)
if (message.isIntermediate)
Padding(
padding: EdgeInsets.only(top: message.content.isNotEmpty ? 8.h : 0),
child: _buildLoadingIndicator(),

28
lib/data/models/appconfig.dart

@ -0,0 +1,28 @@
import 'dart:convert';
import 'package:deep_voice/data/models/appconfig_model.dart';
class AppConfig {
static late UserGetAppConfigResp config;
// 可选:提供初始化方法
static void initialize(UserGetAppConfigResp resp) {
config = resp;
}
//环境变量
static String? env(String key) {
return config.env[key];
}
//mcp配置
static dynamic mcpConfig() {
return {"mcpServers": config.mcps};
}
//智能体配置
static String agent(String agentId) {
final foundAgent = config.agents.firstWhere((agent) => agent.id == agentId);
return jsonEncode((foundAgent));
}
}

65
lib/data/models/appconfig_model.dart

@ -0,0 +1,65 @@
import 'package:json_annotation/json_annotation.dart';
part 'appconfig_model.g.dart'; // 生成的文件名
@JsonSerializable()
class UserGetAppConfigResp {
final Map<String, String> env;
final List<DBAgent> agents;
final Map<String, DBMCPServer> mcps;
UserGetAppConfigResp(
{required this.env, required this.agents, required this.mcps});
factory UserGetAppConfigResp.fromJson(Map<String, dynamic> json) =>
_$UserGetAppConfigRespFromJson(json);
Map<String, dynamic> toJson() => _$UserGetAppConfigRespToJson(this);
}
@JsonSerializable()
class DBAgent {
final String id;
final String name;
final String avatarUrl;
final String description;
final String tag;
final bool isOnline;
final String voice;
final String? welcomeMessage;
final String? voiceWelcomeMessage;
final String? systemPrompt;
final List<String>? tools;
DBAgent({
required this.id,
required this.name,
required this.avatarUrl,
required this.description,
required this.tag,
required this.isOnline,
required this.voice,
required this.welcomeMessage,
required this.voiceWelcomeMessage,
required this.systemPrompt,
required this.tools,
});
factory DBAgent.fromJson(Map<String, dynamic> json) =>
_$DBAgentFromJson(json);
Map<String, dynamic> toJson() => _$DBAgentToJson(this);
}
@JsonSerializable()
class DBMCPServer {
final String servername;
final String url;
final List<String>? tools;
DBMCPServer({
required this.servername,
required this.url,
required this.tools,
});
factory DBMCPServer.fromJson(Map<String, dynamic> json) =>
_$DBMCPServerFromJson(json);
Map<String, dynamic> toJson() => _$DBMCPServerToJson(this);
}

70
lib/data/models/appconfig_model.g.dart

@ -0,0 +1,70 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'appconfig_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserGetAppConfigResp _$UserGetAppConfigRespFromJson(
Map<String, dynamic> json) =>
UserGetAppConfigResp(
env: Map<String, String>.from(json['env'] as Map),
agents: (json['agents'] as List<dynamic>)
.map((e) => DBAgent.fromJson(e as Map<String, dynamic>))
.toList(),
mcps: (json['mcps'] as Map<String, dynamic>).map(
(k, e) => MapEntry(k, DBMCPServer.fromJson(e as Map<String, dynamic>)),
),
);
Map<String, dynamic> _$UserGetAppConfigRespToJson(
UserGetAppConfigResp instance) =>
<String, dynamic>{
'env': instance.env,
'agents': instance.agents,
'mcps': instance.mcps,
};
DBAgent _$DBAgentFromJson(Map<String, dynamic> json) => DBAgent(
id: json['id'] as String,
name: json['name'] as String,
avatarUrl: json['avatarUrl'] as String,
description: json['description'] as String,
tag: json['tag'] as String,
isOnline: json['isOnline'] as bool,
voice: json['voice'] as String,
welcomeMessage: json['welcomeMessage'] as String?,
voiceWelcomeMessage: json['voiceWelcomeMessage'] as String?,
systemPrompt: json['systemPrompt'] as String?,
tools:
(json['tools'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$DBAgentToJson(DBAgent instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'avatarUrl': instance.avatarUrl,
'description': instance.description,
'tag': instance.tag,
'isOnline': instance.isOnline,
'voice': instance.voice,
'welcomeMessage': instance.welcomeMessage,
'voiceWelcomeMessage': instance.voiceWelcomeMessage,
'systemPrompt': instance.systemPrompt,
'tools': instance.tools,
};
DBMCPServer _$DBMCPServerFromJson(Map<String, dynamic> json) => DBMCPServer(
servername: json['servername'] as String,
url: json['url'] as String,
tools:
(json['tools'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$DBMCPServerToJson(DBMCPServer instance) =>
<String, dynamic>{
'servername': instance.servername,
'url': instance.url,
'tools': instance.tools,
};

13
lib/data/models/events/voice_interaction_event.dart

@ -26,4 +26,17 @@ class RecognitionStartedEvent extends VoiceInteractionEvent {
RecognitionStartedEvent({
required int timestamp,
}) : super(timestamp: timestamp);
}
/// 通用语音交互事件
/// 用于处理其他类型的事件
class GenericVoiceInteractionEvent extends VoiceInteractionEvent {
final String type;
final Map<String, dynamic>? data;
GenericVoiceInteractionEvent({
required this.type,
this.data,
required int timestamp,
}) : super(timestamp: timestamp);
}

2
lib/data/models/message_model.dart

@ -55,4 +55,4 @@ class Message {
isLoading: isLoading ?? this.isLoading,
);
}
}
}

6
lib/data/services/ai_service.dart

@ -3,12 +3,14 @@ abstract class AiService {
/// 非流式输出方法
Future<String> sendMessage({
required List<Map<String, String>> messages,
required String systemPrompt,
String? systemPrompt,
String? userProperties,
});
/// 流式输出方法
Stream sendMessageStream({
required List<Map<String, String>> messages,
required String systemPrompt,
String? systemPrompt,
String? userProperties,
});
}

566
lib/data/services/ble_manager.dart

@ -0,0 +1,566 @@
import 'dart:async';
import 'package:deep_voice/data/models/appconfig.dart';
import 'package:get/get.dart';
import 'package:ble_service/ble_service.dart';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
/// 命令类型常量(与原生层保持一致)
class BleCommands {
static const int GET_VERSION = 0x01;
static const int GET_BATTERY_INFO = 0x02;
static const int CONTROL_CODEC = 0x03;
static const int PLAY_A2DP = 0x04;
}
/// 蓝牙设备信息缓存模型
class DeviceInfo {
final String address;
final String name;
// 整体电池电量
final int batteryLevel;
// 详细电池信息
final int leftEarBattery;
final int rightEarBattery;
final int caseBattery;
// 充电状态
final bool leftEarCharging;
final bool rightEarCharging;
final bool caseCharging;
final String firmwareVersion;
final DateTime lastUpdated;
DeviceInfo({
required this.address,
required this.name,
this.batteryLevel = 0,
this.leftEarBattery = 0,
this.rightEarBattery = 0,
this.caseBattery = 0,
this.leftEarCharging = false,
this.rightEarCharging = false,
this.caseCharging = false,
this.firmwareVersion = '',
DateTime? lastUpdated,
}) : lastUpdated = lastUpdated ?? DateTime.now();
/// 创建更新后的设备信息副本
DeviceInfo copyWith({
String? address,
String? name,
int? batteryLevel,
int? leftEarBattery,
int? rightEarBattery,
int? caseBattery,
bool? leftEarCharging,
bool? rightEarCharging,
bool? caseCharging,
String? firmwareVersion,
}) {
return DeviceInfo(
address: address ?? this.address,
name: name ?? this.name,
batteryLevel: batteryLevel ?? this.batteryLevel,
leftEarBattery: leftEarBattery ?? this.leftEarBattery,
rightEarBattery: rightEarBattery ?? this.rightEarBattery,
caseBattery: caseBattery ?? this.caseBattery,
leftEarCharging: leftEarCharging ?? this.leftEarCharging,
rightEarCharging: rightEarCharging ?? this.rightEarCharging,
caseCharging: caseCharging ?? this.caseCharging,
firmwareVersion: firmwareVersion ?? this.firmwareVersion,
lastUpdated: DateTime.now(),
);
}
@override
String toString() {
return 'DeviceInfo(address: $address, name: $name, '
'battery: $batteryLevel%, L: $leftEarBattery%, R: $rightEarBattery%, Case: $caseBattery%, '
'charging: L:$leftEarCharging, R:$rightEarCharging, C:$caseCharging, '
'version: $firmwareVersion)';
}
}
/// BLE管理器 - 专门处理BLE设备连接和信息管理
class BleManager extends GetxService {
static BleManager get to => Get.find<BleManager>();
// BLE服务实例
final BleService _bleService = BleService();
// BLE连接状态
final _connectionState = BleConnectionState.disconnected.obs;
int get connectionState => _connectionState.value;
// 连接状态监听
final _isConnected = false.obs;
bool get isConnected => _isConnected.value;
// 设备信息缓存
final Rx<DeviceInfo?> _deviceInfo = Rx<DeviceInfo?>(null);
DeviceInfo? get deviceInfo => _deviceInfo.value;
// 订阅
StreamSubscription<dynamic>? _statusSubscription;
StreamSubscription<dynamic>? _dataSubscription;
final GetStorage _storage = GetStorage();
/// 初始化
@override
void onInit() {
super.onInit();
Logger.info('BLE管理器初始化');
// 设置BLE事件监听
_setupBleListener();
}
/// 释放资源
@override
void onClose() {
_statusSubscription?.cancel();
_dataSubscription?.cancel();
super.onClose();
}
/// 设置BLE事件监听
void _setupBleListener() {
_statusSubscription?.cancel();
_dataSubscription?.cancel();
try {
// 监听BLE状态更新
_statusSubscription = _bleService.onStatusUpdated.listen(
(event) {
Logger.info('接收到BLE状态更新: $event');
if (event is Map) {
// 处理连接状态更新
if (event.containsKey('connectionState')) {
final state = event['connectionState'] as int;
_connectionState.value = state;
// 连接状态转换
if (state == BleConnectionState.connected) {
Logger.info('BLE设备已连接');
_isConnected.value = true;
// 在连接成功后获取设备信息
_fetchDeviceInfo();
} else if (state == BleConnectionState.disconnected ||
state == BleConnectionState.error) {
Logger.info('BLE设备已断开或发生错误');
_isConnected.value = false;
}
}
// 处理设备信息
_processDeviceInfo(event);
}
},
onError: (error) {
Logger.error('BLE状态事件流错误: $error');
},
);
// 监听BLE数据流
_dataSubscription = _bleService.onDataReceived.listen(
(data) {
Logger.info('接收到BLE数据: $data');
// 在这里处理来自设备的数据
},
onError: (error) {
Logger.error('BLE数据流错误: $error');
},
);
} catch (e) {
Logger.error('设置BLE事件监听器失败: $e');
}
}
/// 处理来自原生层的设备信息
void _processDeviceInfo(Map<dynamic, dynamic> event) {
try {
// 检查是否是设备信息响应
if (event.containsKey('command') && event.containsKey('success')) {
final bool success = event['success'] as bool;
if (!success) {
Logger.warning('设备命令执行失败: ${event['command']}');
return;
}
// 根据命令类型处理不同信息
final command = event['command'];
if (command is int) {
// 基于命令ID处理
_processDeviceInfoByCommandId(command, event);
} else if (command is String) {
// 基于命令名称处理
_processDeviceInfoByCommandName(command, event);
}
}
} catch (e) {
Logger.error('处理设备信息异常: $e');
}
}
/// 根据命令ID处理设备信息
void _processDeviceInfoByCommandId(
int commandId, Map<dynamic, dynamic> data) {
switch (commandId) {
case BleCommands.GET_BATTERY_INFO:
_updateBatteryInfoFromData(data);
break;
case BleCommands.GET_VERSION:
_updateVersionInfoFromData(data);
break;
default:
Logger.info('未处理的命令ID: $commandId');
}
}
/// 根据命令名称处理设备信息
void _processDeviceInfoByCommandName(
String commandName, Map<dynamic, dynamic> data) {
switch (commandName) {
case 'batteryInfo':
_updateBatteryInfoFromData(data);
break;
case 'versionInfo':
_updateVersionInfoFromData(data);
break;
default:
Logger.info('未处理的命令名称: $commandName');
}
}
/// 更新电池信息
void _updateBatteryInfoFromData(Map<dynamic, dynamic> data) {
try {
// 确保数据中包含电池信息
if (!data.containsKey('leftEarBattery') ||
!data.containsKey('rightEarBattery') ||
!data.containsKey('caseBattery')) {
Logger.warning('电池信息数据不完整');
return;
}
// 提取电池信息
final int leftBattery = data['leftEarBattery'] as int? ?? 0;
final int rightBattery = data['rightEarBattery'] as int? ?? 0;
final int caseBattery = data['caseBattery'] as int? ?? 0;
final bool leftCharging = data['leftEarCharging'] as bool? ?? false;
final bool rightCharging = data['rightEarCharging'] as bool? ?? false;
final bool caseCharging = data['caseCharging'] as bool? ?? false;
// 获取整体电量(原生层使用左耳电量作为主要指示)
final int overallBattery = data['batteryLevel'] as int? ?? leftBattery;
// 更新设备信息
_updateDeviceInfoIfNeeded(
batteryLevel: overallBattery,
leftEarBattery: leftBattery,
rightEarBattery: rightBattery,
caseBattery: caseBattery,
leftEarCharging: leftCharging,
rightEarCharging: rightCharging,
caseCharging: caseCharging,
);
Logger.info(
'设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
} catch (e) {
Logger.error('更新电池信息失败: $e');
}
}
/// 更新版本信息
void _updateVersionInfoFromData(Map<dynamic, dynamic> data) {
try {
// 确保数据中包含版本信息
if (!data.containsKey('version')) {
Logger.warning('版本信息数据不完整');
return;
}
// 提取版本信息
final String version = data['version'] as String? ?? '';
// 更新设备信息
_updateDeviceInfoIfNeeded(
firmwareVersion: version,
);
Logger.info('设备版本信息已更新: 版本=$version');
} catch (e) {
Logger.error('更新版本信息失败: $e');
}
}
/// 更新设备信息(如果已存在设备)
void _updateDeviceInfoIfNeeded({
int? batteryLevel,
int? leftEarBattery,
int? rightEarBattery,
int? caseBattery,
bool? leftEarCharging,
bool? rightEarCharging,
bool? caseCharging,
String? firmwareVersion,
}) {
// 如果设备信息已存在,则更新
if (_deviceInfo.value != null) {
_deviceInfo.value = _deviceInfo.value!.copyWith(
batteryLevel: batteryLevel,
leftEarBattery: leftEarBattery,
rightEarBattery: rightEarBattery,
caseBattery: caseBattery,
leftEarCharging: leftEarCharging,
rightEarCharging: rightEarCharging,
caseCharging: caseCharging,
firmwareVersion: firmwareVersion,
);
}
// 如果设备信息不存在,但有地址和名称,则创建新的设备信息
else if (_deviceInfo.value == null &&
_isConnected.value &&
(firmwareVersion != null || batteryLevel != null)) {
// 提示尝试创建设备信息但缺少必要信息
Logger.warning('尝试更新设备信息,但设备基本信息不存在');
}
}
/// 连接成功后获取设备信息
Future<void> _fetchDeviceInfo() async {
Logger.info('开始获取BLE设备详细信息');
try {
// 获取电池信息
final batteryResult = await _bleService.getBatteryInfo();
if (batteryResult) {
Logger.info('电池信息获取指令已发送');
} else {
Logger.warning('电池信息获取指令发送失败');
}
// 获取设备版本信息
final versionResult = await _bleService.getDeviceVersion();
if (versionResult) {
Logger.info('版本信息获取指令已发送');
} else {
Logger.warning('版本信息获取指令发送失败');
}
} catch (e) {
Logger.error('获取BLE设备信息失败: $e');
}
}
/// 检查BLE连接状态
Future<int> getConnectionState() async {
try {
final isConnected = await _bleService.isConnected();
_connectionState.value = isConnected
? BleConnectionState.connected
: BleConnectionState.disconnected;
_isConnected.value = isConnected;
return _connectionState.value;
} catch (e) {
Logger.error('获取BLE连接状态失败: $e');
return BleConnectionState.error;
}
}
/// 启动BLE服务
Future<bool> startBleService() async {
try {
// 从.env文件直接读取环境变量
// final azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
// final azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? 'eastasia';
// final openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
// final openaiBaseUrl =
// dotenv.env['OPENAI_BASE_URL'] ?? 'https://api.openai.com/v1';
// final openaiModel = dotenv.env['OPENAI_MODEL'] ?? 'gpt-3.5-turbo';
// 从Flutter资源包中加载.mcp.json
// final mcpConfig = await rootBundle.loadString('.mcp.json');
// 验证JSON格式
// final jsonData = jsonDecode(mcpConfig);
final azureSpeechKey = AppConfig.env('AZURE_SPEECH_KEY') ?? '';
final azureSpeechRegion =
AppConfig.env('AZURE_SPEECH_REGION') ?? 'eastasia';
final openaiApiKey = AppConfig.env('OPENAI_API_KEY') ?? '';
final openaiBaseUrl =
AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1';
final openaiModel = AppConfig.env('OPENAI_MODEL') ?? 'gpt-3.5-turbo';
// 从Flutter资源包中加载.mcp.json
final jsonData = AppConfig.mcpConfig();
final config = <String, dynamic>{
// 语音代理配置参数
'azureSpeechKey': azureSpeechKey,
'azureSpeechRegion': azureSpeechRegion,
'openaiApiKey': openaiApiKey,
'openaiBaseUrl': openaiBaseUrl,
'openaiModel': openaiModel,
'mcpServer': jsonEncode(jsonData),
};
// 记录日志(不记录敏感信息)
Logger.info('启动BLE服务,配置参数键名: ${config}');
if (azureSpeechKey.isEmpty || openaiApiKey.isEmpty) {
Logger.warning('缺少必要的API密钥,请检查.env文件配置');
// 检查.env中相关密钥是否存在
if (azureSpeechKey.isEmpty) {
Logger.warning('AZURE_SPEECH_KEY未配置在.env文件中');
}
if (openaiApiKey.isEmpty) {
Logger.warning('OPENAI_API_KEY未配置在.env文件中');
}
}
final result = await _bleService.startBleService(config);
if (result) {
Logger.info('BLE服务启动成功');
// 添加延迟以等待设备连接
await Future.delayed(const Duration(seconds: 2));
// 检查连接状态
final isConnected = await _bleService.isConnected();
if (isConnected) {
_isConnected.value = true;
_connectionState.value = BleConnectionState.connected;
// 如果设备信息存在,则获取详细信息
if (_deviceInfo.value != null) {
// 获取详细设备信息
_fetchDeviceInfo();
}
}
} else {
Logger.error('BLE服务启动失败');
}
return result;
} catch (e) {
Logger.error('启动BLE服务失败: $e');
return false;
}
}
/// 停止BLE服务
Future<bool> stopBleService() async {
try {
final result = await _bleService.stopBleService();
if (result) {
Logger.info('BLE服务停止成功');
_isConnected.value = false;
_connectionState.value = BleConnectionState.disconnected;
} else {
Logger.error('BLE服务停止失败');
}
return result;
} catch (e) {
Logger.error('停止BLE服务失败: $e');
return false;
}
}
registerBackgroundScan() {
_bleService.registerBackgroundScan();
}
/// 获取电池电量
Future<int> getBatteryLevel() async {
try {
return await _bleService.getBatteryLevel();
} catch (e) {
Logger.error('获取电池电量失败: $e');
return 0;
}
}
/// 主动获取电池信息
Future<bool> fetchBatteryInfo() async {
try {
return await _bleService.getBatteryInfo();
} catch (e) {
Logger.error('获取电池信息失败: $e');
return false;
}
}
/// 主动获取设备版本信息
Future<bool> fetchDeviceVersion() async {
try {
return await _bleService.getDeviceVersion();
} catch (e) {
Logger.error('获取设备版本失败: $e');
return false;
}
}
/// 打开编码器
Future<bool> openEncoder() async {
try {
return await _bleService.openEncoder();
} catch (e) {
Logger.error('打开编码器失败: $e');
return false;
}
}
/// 打开解码器
Future<bool> openDecoder() async {
try {
return await _bleService.openDecoder();
} catch (e) {
Logger.error('打开解码器失败: $e');
return false;
}
}
/// 关闭编解码器
Future<bool> closeCodec() async {
try {
return await _bleService.closeCodec();
} catch (e) {
Logger.error('关闭编解码器失败: $e');
return false;
}
}
/// 播放A2DP
Future<bool> playA2DP() async {
try {
return await _bleService.playA2DP();
} catch (e) {
Logger.error('播放A2DP失败: $e');
return false;
}
}
/// 注册服务
static Future<BleManager> init() async {
if (!Get.isRegistered<BleManager>()) {
Get.put(BleManager(), permanent: true);
}
return Future.value(Get.find<BleManager>());
}
}

125
lib/data/services/bluetooth_manager.dart

@ -0,0 +1,125 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart';
import 'package:deep_voice/core/utils/logger.dart';
/// 蓝牙服务 - 管理经典蓝牙服务
class BluetoothManager extends GetxService {
static BluetoothManager get to => Get.find<BluetoothManager>();
// 蓝牙状态
final _bluetoothState = 'unknown'.obs;
String get bluetoothState => _bluetoothState.value;
// 当前经典蓝牙设备
final Rx<BluetoothDevice?> currentDeviceRx = Rx<BluetoothDevice?>(null);
BluetoothDevice? get currentDevice => currentDeviceRx.value;
// 订阅
StreamSubscription<BluetoothEvent>? _bluetoothSubscription;
/// 初始化
@override
void onInit() {
super.onInit();
Logger.info('蓝牙服务初始化');
// 设置蓝牙事件监听
_setupBluetoothListener();
}
/// 释放资源
@override
void onClose() {
_bluetoothSubscription?.cancel();
super.onClose();
}
/// 设置经典蓝牙事件监听
void _setupBluetoothListener() {
_bluetoothSubscription?.cancel();
try {
_bluetoothSubscription = ClassicBluetooth.events.listen(
(event) {
if (event.type == 'stateChanged' && event.state != null) {
_bluetoothState.value = event.state!;
Logger.info('蓝牙状态变更: ${event.state}');
// 如果蓝牙关闭,清除设备
if (event.state == 'off') {
currentDeviceRx.value = null;
}
} else if (event.type == 'deviceConnected' && event.device != null) {
// 直接从事件中获取设备信息
final newDevice = event.device!;
// 更新当前设备
currentDeviceRx.value = newDevice;
Logger.info('经典蓝牙设备已连接: ${newDevice.name} (${newDevice.address})');
} else if (event.type == 'deviceDisconnected' && event.device != null) {
// 直接从事件中获取设备信息
final disconnectedDevice = event.device!;
Logger.info('经典蓝牙设备已断开: ${disconnectedDevice.name} (${disconnectedDevice.address})');
// 如果断开的是当前设备,清除当前设备
if (currentDeviceRx.value?.address == disconnectedDevice.address) {
currentDeviceRx.value = null;
Logger.info('已清除当前设备引用');
}
}
},
onError: (error) {
Logger.error('蓝牙事件流错误: $error');
},
);
} catch (e) {
Logger.error('设置蓝牙事件监听器失败: $e');
}
}
/// 刷新设备连接状态
Future<void> refreshDeviceConnection() async {
try {
// 如果BLE未连接,则检查经典蓝牙连接
final devices = await ClassicBluetooth.getConnectedDevices();
if (devices.isNotEmpty) {
final newDevice = devices.first;
// 更新当前设备
currentDeviceRx.value = newDevice;
Logger.info('已连接经典蓝牙设备: ${newDevice.name} (${newDevice.address})');
return;
}
// 没有任何连接
currentDeviceRx.value = null;
Logger.info('未找到已连接的蓝牙设备');
} catch (e) {
Logger.error('刷新设备连接失败: $e');
}
}
/// 检查蓝牙是否已启用
Future<bool> isBluetoothEnabled() async {
try {
final isEnabled = await ClassicBluetooth.isBluetoothEnabled();
_bluetoothState.value = isEnabled ? 'on' : 'off';
return isEnabled;
} catch (e) {
Logger.error('检查蓝牙状态失败: $e');
return false;
}
}
}

310
lib/data/services/bluetooth_media_button_service.dart

@ -1,310 +0,0 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
import 'voice_interaction_service.dart';
/// 蓝牙媒体按钮事件类型
enum MediaButtonType {
/// 播放/暂停按钮
playPause,
/// 未知按钮
unknown
}
/// 蓝牙媒体按钮事件
class MediaButtonEvent {
/// 按钮类型
final MediaButtonType buttonType;
/// 事件发生时间戳
final int timestamp;
MediaButtonEvent({
required this.buttonType,
required this.timestamp,
});
factory MediaButtonEvent.fromMap(Map<dynamic, dynamic> map) {
MediaButtonType buttonType;
switch (map['buttonType']) {
case 'playPause':
buttonType = MediaButtonType.playPause;
break;
default:
// 将所有其他事件类型映射为 playPause
if (map['buttonType'] == 'play' ||
map['buttonType'] == 'pause' ||
map['buttonType'] == 'togglePlayPause') {
buttonType = MediaButtonType.playPause;
} else {
// 忽略其他类型(next, previous, seekForward, seekBackward)
buttonType = MediaButtonType.unknown;
}
}
return MediaButtonEvent(
buttonType: buttonType,
timestamp: map['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch,
);
}
@override
String toString() {
switch (buttonType) {
case MediaButtonType.playPause:
return '蓝牙媒体按钮: 播放/暂停';
case MediaButtonType.unknown:
return '蓝牙媒体按钮: 未知';
}
}
}
/// 蓝牙媒体按钮服务
class BluetoothMediaButtonService extends GetxService {
static BluetoothMediaButtonService get to => Get.find<BluetoothMediaButtonService>();
// 方法通道名称
final MethodChannel _channel = const MethodChannel('com.deep_voice.bluetooth_media_button');
// 事件通道名称
final EventChannel _eventChannel = const EventChannel('com.deep_voice.bluetooth_media_button_events');
/// 按钮事件流控制器
final _buttonEventController = StreamController<MediaButtonEvent>.broadcast();
/// 按钮事件流
Stream<MediaButtonEvent> get buttonEvents => _buttonEventController.stream;
/// 是否已启用监听
final _isListening = false.obs;
bool get isListening => _isListening.value;
/// 是否正在加载
final isLoading = false.obs;
/// 错误信息
final errorMessage = ''.obs;
/// 按钮事件流订阅
StreamSubscription? _buttonEventSubscription;
/// 服务是否已初始化
bool _isInitialized = false;
/// 是否支持媒体按钮服务(iOS 13.0及以上)
bool get isSupported {
if (!Platform.isIOS) return true; // 非iOS平台默认支持
// 获取平台版本信息
final String version = Platform.operatingSystemVersion;
// 尝试提取iOS版本号 (格式如: "Version 14.5 (Build 18E182)")
try {
// 提取版本号
final RegExp versionRegex = RegExp(r'Version\s+(\d+)\.(\d+)');
final match = versionRegex.firstMatch(version);
if (match != null) {
final int majorVersion = int.parse(match.group(1) ?? '0');
return majorVersion >= 13; // iOS 13及以上支持
}
} catch (e) {
Logger.error('解析iOS版本失败: $e');
}
// 无法确定版本时,假设不支持
return false;
}
/// 初始化服务
BluetoothMediaButtonService() {
Logger.info('创建 BluetoothMediaButtonService 实例');
}
/// 初始化媒体按钮服务
Future<bool> initialize() async {
if (_isInitialized) {
Logger.info('BluetoothMediaButtonService 已经初始化');
return true;
}
// 检查系统版本是否支持
if (!isSupported) {
Logger.info('当前iOS版本不支持蓝牙媒体按钮功能(需要iOS 13.0+)');
return false;
}
try {
Logger.info('正在初始化 BluetoothMediaButtonService...');
// 设置事件监听器
_setupButtonEventListener();
_isInitialized = true;
Logger.info('BluetoothMediaButtonService 初始化成功');
return true;
} catch (e) {
Logger.error('初始化 BluetoothMediaButtonService 失败: $e');
errorMessage.value = '初始化蓝牙媒体按钮服务失败';
return false;
}
}
/// 设置按钮事件监听器
void _setupButtonEventListener() {
_buttonEventSubscription?.cancel();
// 设置事件通道监听器
_buttonEventSubscription = _eventChannel
.receiveBroadcastStream()
.listen(_handleButtonEvent, onError: _handleEventStreamError);
Logger.info('已设置蓝牙媒体按钮事件监听器');
}
/// 处理按钮事件
void _handleButtonEvent(dynamic event) {
Logger.info('BluetoothMediaButtonService 收到原始事件: $event');
if (event is! Map) {
Logger.error('收到无效的蓝牙媒体按钮事件 (非Map类型): $event');
return;
}
final Map<dynamic, dynamic> eventMap = event;
try {
if (eventMap['type'] == 'mediaButtonEvent') {
// 获取按钮类型
final String buttonType = eventMap['buttonType'] as String? ?? 'unknown';
// 判断是否为playPause相关事件
if (buttonType == 'playPause' ||
buttonType == 'play' ||
buttonType == 'pause' ||
buttonType == 'togglePlayPause') {
// 创建playPause事件并广播
final buttonEvent = MediaButtonEvent(
buttonType: MediaButtonType.playPause,
timestamp: eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch,
);
// 广播事件
_buttonEventController.add(buttonEvent);
// 广播事件
_buttonEventController.add(buttonEvent);
Logger.info('发送playPause按钮事件: $buttonEvent');
} else {
// 忽略其他类型的按钮事件
Logger.info('忽略非playPause按钮事件: $buttonType');
}
} else {
Logger.warning('收到未知类型的事件: ${eventMap['type']}');
}
} catch (e) {
Logger.error('处理蓝牙媒体按钮事件时出错: $e');
}
}
/// 处理事件流错误
void _handleEventStreamError(dynamic error) {
Logger.error('蓝牙媒体按钮事件流错误: $error');
errorMessage.value = '蓝牙媒体按钮监听出错';
}
/// 开始监听蓝牙媒体按钮事件
Future<bool> startButtonListening() async {
// 检查系统版本是否支持
if (!isSupported) {
Logger.info('当前iOS版本不支持蓝牙媒体按钮功能(需要iOS 13.0+)');
return false;
}
if (!_isInitialized && !(await initialize())) {
return false;
}
if (_isListening.value) {
Logger.info('已经在监听蓝牙媒体按钮事件');
return true;
}
isLoading.value = true;
try {
final result = await _channel.invokeMethod<bool>('startButtonListening') ?? false;
if (result) {
_isListening.value = true;
Logger.info('开始监听蓝牙媒体按钮事件成功');
} else {
errorMessage.value = '开始监听蓝牙媒体按钮事件失败';
Logger.error('开始监听蓝牙媒体按钮事件失败');
}
isLoading.value = false;
return result;
} catch (e) {
isLoading.value = false;
errorMessage.value = '开始监听蓝牙媒体按钮事件出错: $e';
Logger.error('开始监听蓝牙媒体按钮事件出错: $e');
return false;
}
}
/// 停止监听蓝牙媒体按钮事件
Future<bool> stopButtonListening() async {
if (!_isInitialized) {
return false;
}
if (!_isListening.value) {
return true;
}
isLoading.value = true;
try {
final result = await _channel.invokeMethod<bool>('stopButtonListening') ?? false;
if (result) {
_isListening.value = false;
Logger.info('停止监听蓝牙媒体按钮事件成功');
} else {
errorMessage.value = '停止监听蓝牙媒体按钮事件失败';
Logger.error('停止监听蓝牙媒体按钮事件失败');
}
isLoading.value = false;
return result;
} catch (e) {
isLoading.value = false;
errorMessage.value = '停止监听蓝牙媒体按钮事件出错: $e';
Logger.error('停止监听蓝牙媒体按钮事件出错: $e');
return false;
}
}
/// 释放资源
void dispose() {
_buttonEventSubscription?.cancel();
_buttonEventSubscription = null;
_buttonEventController.close();
_isInitialized = false;
_isListening.value = false;
Logger.info('BluetoothMediaButtonService 已释放');
}
@override
void onClose() {
dispose();
super.onClose();
}
}

105
lib/data/services/chat_history_service.dart

@ -1,16 +1,13 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../modules/chat/models/message_model.dart';
import '../../core/utils/logger.dart';
import 'package:chat_storage/chat_storage.dart';
/// 聊天记录管理服务,负责保存和加载聊天记录
class ChatHistoryService extends GetxService {
final GetStorage _storage = GetStorage();
// 存储键名
static const String _historyKeyPrefix = 'chat_history_';
static const int _maxHistoryCount = 10; // 每个对话最多保存的消息数
// 最大历史记录数量
static const int _maxHistoryCount = 200; // 每个对话最多保存的消息数
@override
void onInit() {
@ -18,50 +15,95 @@ class ChatHistoryService extends GetxService {
Logger.info('ChatHistoryService 已初始化');
}
/// 生成历史记录存储键
String _getHistoryKey(String agentId) => '$_historyKeyPrefix$agentId';
/// 保存聊天记录
///
/// [agentId] 代理ID
/// [messages] 消息列表
Future<void> saveHistory(String agentId, List<Message> messages) async {
try {
// 过滤掉加载中的消息
final filteredMessages = messages.where((m) => !m.isLoading).toList();
if (messages.isEmpty) return;
// 如果消息超过最大数量,只保留最新的消息
final messagesToSave = filteredMessages.length > _maxHistoryCount
? filteredMessages.sublist(filteredMessages.length - _maxHistoryCount)
: filteredMessages;
// 获取最新的消息
final latestMessage = messages.last;
// 转换为JSON
final jsonList = messagesToSave.map((m) => m.toJson()).toList();
final jsonString = jsonEncode(jsonList);
// 如果是加载中的消息则跳过
if (latestMessage.isIntermediate) return;
// 保存到存储
await _storage.write(_getHistoryKey(agentId), jsonString);
// Logger.info('已保存聊天记录,代理: $agentId,消息数: ${messagesToSave.length}');
// 直接插入最新的消息到数据库
await ChatStorage.saveMessage(
sessionId: agentId,
message: latestMessage.content,
sender: latestMessage.role,
);
// 检查并清理过多的历史记录
await _cleanupOldMessages(agentId);
} catch (e) {
Logger.error('保存聊天记录失败', e);
}
}
/// 清理旧消息,保持每个会话的消息数量不超过最大限制
Future<void> _cleanupOldMessages(String agentId) async {
try {
final pagedResult = await ChatStorage.getMessages(
sessionId: agentId,
page: 1,
pageSize: 1, // 只获取总数信息
);
// 如果消息总数超过最大限制,删除最早的消息
if (pagedResult.totalCount > _maxHistoryCount) {
final messagesToDelete = pagedResult.totalCount - _maxHistoryCount;
// 获取最早的N条消息ID
final oldestMessages = await ChatStorage.getMessages(
sessionId: agentId,
page: 1,
pageSize: messagesToDelete,
);
// 构建要删除的消息ID列表
final List<int> messageIds = oldestMessages.messages
.where((m) => m.id != null)
.map((m) => m.id!)
.toList();
// 删除这些消息
if (messageIds.isNotEmpty) {
await ChatStorage.deleteMessages(messageIds: messageIds);
}
}
} catch (e) {
Logger.error('清理旧消息失败', e);
}
}
/// 加载聊天记录
///
/// [agentId] 代理ID
/// 返回消息列表,如果没有找到或解析错误则返回空列表
List<Message> loadHistory(String agentId) {
Future<List<Message>> loadHistory(String agentId) async {
try {
final jsonString = _storage.read<String>(_getHistoryKey(agentId));
final pagedResult = await ChatStorage.getMessages(
sessionId: agentId,
page: 1,
pageSize: _maxHistoryCount,
);
if (jsonString == null) {
if (pagedResult.messages.isEmpty) {
return [];
}
final jsonList = jsonDecode(jsonString) as List;
final messages = jsonList.map((json) => Message.fromJson(json)).toList();
// 将ChatStorage.ChatMessage转换为Message
final messages = pagedResult.messages.map((chatMessage) {
return Message(
role: chatMessage.sender,
content: chatMessage.message,
timestamp: DateTime.parse(chatMessage.timestamp), // 直接使用chatMessage的timestamp
// isLoading 默认false
// isLoading: metadata['isLoading'] ?? false,
);
}).toList();
return messages;
} catch (e) {
@ -73,7 +115,7 @@ class ChatHistoryService extends GetxService {
/// 清除特定代理的聊天记录
Future<void> clearHistory(String agentId) async {
try {
await _storage.remove(_getHistoryKey(agentId));
await ChatStorage.deleteMessages(sessionId: agentId);
Logger.info('已清除聊天记录,代理: $agentId');
} catch (e) {
Logger.error('清除聊天记录失败', e);
@ -83,12 +125,7 @@ class ChatHistoryService extends GetxService {
/// 清除所有聊天记录
Future<void> clearAllHistory() async {
try {
final keys = _storage.getKeys();
for (final key in keys) {
if (key.startsWith(_historyKeyPrefix)) {
await _storage.remove(key);
}
}
await ChatStorage.clearDatabase();
Logger.info('已清除所有聊天记录');
} catch (e) {
Logger.error('清除所有聊天记录失败', e);

243
lib/data/services/classic_bluetooth_service.dart

@ -1,243 +0,0 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../core/utils/logger.dart';
import 'dart:io';
/// 蓝牙设备信息
class BluetoothDevice {
final String name;
final String address;
final String type;
BluetoothDevice(
{required this.name, required this.address, required this.type});
@override
String toString() =>
'BluetoothDevice(name: $name, address: $address, type: $type)';
}
/// 经典蓝牙服务
class ClassicBluetoothService extends GetxService {
static ClassicBluetoothService get to => Get.find<ClassicBluetoothService>();
// 通道名称
final MethodChannel _channel =
const MethodChannel('com.deep_voice.classic_bluetooth');
final EventChannel _eventChannel =
const EventChannel('com.deep_voice.classic_bluetooth_events');
/// 当前连接的设备
final currentDevice = Rx<BluetoothDevice?>(null);
/// 已连接耳机设备列表
final connectedDevices = <BluetoothDevice>[].obs;
/// 蓝牙设备状态监听
StreamSubscription? _bluetoothStateListener;
/// 蓝牙开关状态
final bluetoothState = 'unknown'.obs;
/// 蓝牙是否可用
bool get isBluetoothOn => bluetoothState.value == 'on';
/// 服务是否已初始化
bool _isInitialized = false;
/// 初始化蓝牙服务 - 单一入口点
Future<bool> initialize() async {
if (_isInitialized) {
Logger.info('蓝牙服务已经初始化,跳过');
return true;
}
try {
Logger.info('正在初始化蓝牙服务...');
// 获取当前蓝牙状态
final bool isEnabled =
await _channel.invokeMethod<bool>('isBluetoothEnabled') ?? false;
bluetoothState.value = isEnabled ? 'on' : 'off';
Logger.info('初始蓝牙状态: ${bluetoothState.value}');
await _channel.invokeMethod('initialize');
// 设置事件监听
_setupBluetoothStateListener();
_isInitialized = true;
Logger.info('蓝牙服务初始化成功');
return true;
} catch (e) {
Logger.error('初始化蓝牙服务失败: $e');
return false;
}
}
/// 检查蓝牙权限
Future<bool> checkPermissions() async {
// 检查蓝牙相关权限
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetooth,
Permission.bluetoothConnect,
Permission.bluetoothScan,
Permission.microphone,
].request();
return statuses[Permission.bluetoothConnect] == PermissionStatus.granted;
}
/// 刷新已连接的蓝牙耳机设备
Future<bool> refreshConnectedDevices() async {
// 确保服务已初始化
if (!_isInitialized && !(await initialize())) {
return false;
}
// 如果蓝牙未开启,无法获取设备
if (!isBluetoothOn) {
Logger.info('蓝牙未开启,无法获取设备');
return false;
}
try {
// 只获取耳机设备
final devices = await getConnectedHeadsetDevices();
// 更新设备列表
connectedDevices.value = devices;
// 更新当前设备
if (connectedDevices.isNotEmpty && currentDevice.value == null) {
currentDevice.value = connectedDevices.first;
}
return connectedDevices.isNotEmpty;
} catch (e) {
Logger.error('刷新已连接设备失败: $e');
return false;
}
}
/// 获取已连接的耳机设备
Future<List<BluetoothDevice>> getConnectedHeadsetDevices() async {
try {
// 确保服务已初始化
if (!_isInitialized && !(await initialize())) {
return [];
}
final result = await _channel.invokeMethod('getConnectedHeadsetDevices');
if (result is List) {
return result
.map((device) => BluetoothDevice(
name: device['name'] ?? '未知设备',
address: device['address'] ?? '',
type: 'headset'))
.toList();
}
return [];
} catch (e) {
Logger.error('获取已连接的耳机设备失败: $e');
return [];
}
}
/// 设置蓝牙状态监听
void _setupBluetoothStateListener() {
_bluetoothStateListener?.cancel();
_bluetoothStateListener =
_eventChannel.receiveBroadcastStream().listen((event) {
if (event is Map) {
Logger.info('蓝牙状态监听: $event');
final type = event['type'] as String?;
if (type == 'bluetoothStateChanged') {
final state = event['state'] as String?;
if (state != null) {
_handleBluetoothStateChange(state);
}
} else if (type == 'deviceConnected') {
_handleDeviceConnected(event);
} else if (type == 'deviceDisconnected') {
_handleDeviceDisconnected(event);
}
}
}, onError: (error) {
Logger.error('蓝牙状态监听错误: $error');
});
}
/// 处理设备连接事件
void _handleDeviceConnected(Map event) {
final deviceMap = event['device'] as Map?;
if (deviceMap != null) {
final device = BluetoothDevice(
name: deviceMap['name'] ?? '未知设备',
address: deviceMap['address'] ?? '',
type: deviceMap['type'] ?? 'unknown');
// 更新设备列表
if (!connectedDevices.any((d) => d.address == device.address)) {
connectedDevices.add(device);
}
// 更新当前设备
currentDevice.value = device;
Logger.info('设备已连接: ${device.name}');
}
}
/// 处理设备断开事件
void _handleDeviceDisconnected(Map event) {
final deviceMap = event['device'] as Map?;
if (deviceMap != null) {
final address = deviceMap['address'] as String?;
if (address != null) {
// 更新设备列表
connectedDevices.removeWhere((d) => d.address == address);
// 如果是当前设备,清除当前设备
if (currentDevice.value?.address == address) {
currentDevice.value =
connectedDevices.isEmpty ? null : connectedDevices.first;
Logger.info('当前设备已断开连接');
}
}
}
}
/// 处理蓝牙状态变化
void _handleBluetoothStateChange(String state) {
Logger.info('----蓝牙状态变化: $state');
// 更新蓝牙状态
bluetoothState.value = state;
if (state == 'off') {
// 蓝牙关闭,清空设备信息
connectedDevices.clear();
currentDevice.value = null;
} else if (state == 'on') {
// 蓝牙重新打开,延迟一小段时间后刷新设备
Future.delayed(const Duration(milliseconds: 1000), () {
refreshConnectedDevices();
});
}
}
@override
void onClose() {
// 取消蓝牙状态监听
_bluetoothStateListener?.cancel();
super.onClose();
}
}

8
lib/data/services/deapsound_ai_service.dart

@ -17,7 +17,8 @@ class DeapsoundAIService implements AiService {
@override
Future<String> sendMessage({
required List<Map<String, String>> messages,
required String systemPrompt,
String? systemPrompt,
String? userProperties,
}) async {
return '';
}
@ -25,10 +26,11 @@ class DeapsoundAIService implements AiService {
@override
Stream sendMessageStream({
required List<Map<String, String>> messages,
required String systemPrompt,
String? systemPrompt,
String? userProperties,
}) async* {
final client = http.Client();
final url = Uri.parse('https://www.deapsound.com/ai/funcchat');
final url = Uri.parse('https://api.deapsound.com/ai/funcchat');
List contextsList = [];
String msg = '';

97
lib/data/services/function_service.dart

@ -1,97 +0,0 @@
import 'dart:convert';
/// 函数服务
/// 仅实现各种功能函数,不包含AI交互逻辑
class FunctionService {
/// 天气查询工具定义
/// 返回一个可直接用于tools参数的天气查询工具定义
static Map<String, dynamic> getWeatherToolDefinition() {
return {
'type': 'function',
'function': {
'name': 'get_current_weather',
'description': '获取指定地点的当前天气信息',
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': '地理位置,例如:北京市',
},
'unit': {
'type': 'string',
'description': '温度单位',
'enum': ['celsius', 'fahrenheit'],
},
},
'required': ['location'],
},
},
};
}
/// 模拟天气API实现
/// 返回一个模拟的天气信息,实际应用中应该调用真实的天气API
static Future<Map<String, dynamic>> getWeatherInfo(Map<String, dynamic> args) async {
final location = args['location'] ?? '未知地点';
final unit = args['unit'] ?? 'celsius';
// 在实际应用中,这里应该调用真实的天气API
return {
'location': location,
'temperature': unit == 'celsius' ? '25°C' : '77°F',
'conditions': '晴转多云',
'humidity': '55%',
'wind': '西南风 3级',
'date': DateTime.now().toString().split(' ')[0],
};
}
/// 解析函数调用参数
/// 从AI响应中提取函数调用参数
static Map<String, dynamic> parseFunctionCallArguments(Map<String, dynamic> functionCallResponse) {
if (functionCallResponse['tool_calls'] == null ||
functionCallResponse['tool_calls'].isEmpty) {
throw FunctionServiceException('No tool calls found in response');
}
final toolCall = functionCallResponse['tool_calls'][0];
if (toolCall['function'] == null || toolCall['function']['arguments'] == null) {
throw FunctionServiceException('Invalid function call format');
}
try {
return json.decode(toolCall['function']['arguments']);
} catch (e) {
throw FunctionServiceException('Failed to parse function arguments: $e');
}
}
/// 获取函数调用ID和名称
/// 用于构建工具响应消息
static Map<String, String> getFunctionCallInfo(Map<String, dynamic> functionCallResponse) {
if (functionCallResponse['tool_calls'] == null ||
functionCallResponse['tool_calls'].isEmpty) {
throw FunctionServiceException('No tool calls found in response');
}
final toolCall = functionCallResponse['tool_calls'][0];
if (toolCall['id'] == null || toolCall['function'] == null || toolCall['function']['name'] == null) {
throw FunctionServiceException('Invalid function call format');
}
return {
'id': toolCall['id'],
'name': toolCall['function']['name'],
};
}
}
/// 函数调用服务异常类
class FunctionServiceException implements Exception {
final String message;
FunctionServiceException(this.message);
@override
String toString() => message;
}

6
lib/data/services/kouzi_ai_service.dart

@ -13,7 +13,8 @@ class KouziAIService implements AiService {
@override
Future<String> sendMessage({
required List<Map<String, String>> messages,
required String systemPrompt,
String? systemPrompt,
String? userProperties,
}) async {
return '';
}
@ -21,7 +22,8 @@ class KouziAIService implements AiService {
@override
Stream<String> sendMessageStream({
required List<Map<String, String>> messages,
required String systemPrompt,
String? systemPrompt,
String? userProperties,
}) async* {
final client = http.Client();
final url = Uri.parse('https://api.coze.cn/v3/chat');

18
lib/data/services/network/api.dart

@ -55,4 +55,22 @@ class Api {
params: params,
);
}
//用户反馈
static feedback([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_feedback',
params: params,
);
}
//app配置
static getappconfig([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_getappconfig',
params: params,
);
}
}

2
lib/data/services/network/nw_api.dart

@ -1,4 +1,4 @@
class NWApi {
// static const baseApi = "https://www.deapsound.com";
static const baseApi = "https://www.deapsound.com";
static const baseApi = "https://api.deapsound.com";
}

301
lib/data/services/open_ai_service_adapter.dart

@ -0,0 +1,301 @@
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:deep_voice/data/models/appconfig.dart';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:open_ai_service/open_ai_service.dart';
import 'package:get/get.dart';
import 'ai_service.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
/// 定义流事件类型,用于区分不同类型的事件
enum StreamEventType { token, functionCall, complete, error }
/// 流事件包装类
class StreamEvent {
final StreamEventType type;
final String? content;
final Map? meta;
StreamEvent(this.type, {this.content, this.meta});
}
/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件
class OpenAIServiceAdapter implements AiService {
final OpenAIService _openAIService = OpenAIService();
StreamSubscription<OpenAIEvent>? _eventSubscription;
final StreamController<StreamEvent> _tokenStreamController =
StreamController<StreamEvent>.broadcast();
final GetStorage _storage = GetStorage();
/// 构造函数
OpenAIServiceAdapter() {
printInfo(info: '创建OpenAIServiceAdapter实例');
_setupEventListener();
}
/// 设置事件监听器
void _setupEventListener() {
try {
// 首先确保访问eventStream以初始化底层事件通道
_openAIService.eventStream;
// 设置事件处理
_eventSubscription = _openAIService.eventStream.listen(
(event) {
try {
switch (event.type) {
case OpenAIEventType.token:
if (event.content is String) {
_tokenStreamController.add(StreamEvent(StreamEventType.token,
content: event.content as String));
} else {
printInfo(info: '收到非字符串类型的token: ${event.content}');
}
break;
case OpenAIEventType.complete:
_tokenStreamController
.add(StreamEvent(StreamEventType.complete));
break;
case OpenAIEventType.error:
_tokenStreamController.add(StreamEvent(StreamEventType.error,
content: '未知错误: ${event.content}'));
break;
case OpenAIEventType.functionCall:
printInfo(
info:
'接收到 functionCall content: ${event.content} meta: ${event.meta}');
_tokenStreamController.add(StreamEvent(
StreamEventType.functionCall,
meta: event.meta as Map<String, dynamic>?));
// _tokenStreamController.add(StreamEvent(
// StreamEventType.error,
// content: '收到函数调用,该流仅支持文本响应'
// ));
break;
}
} catch (e) {
printError(info: '处理事件出错: $e');
_tokenStreamController
.add(StreamEvent(StreamEventType.error, content: '处理事件失败: $e'));
}
},
onError: (error) {
printError(info: '事件流错误: $error');
_tokenStreamController.add(
StreamEvent(StreamEventType.error, content: '事件流错误: $error'));
},
onDone: () {
printInfo(info: '事件流已关闭');
},
);
} catch (e) {
printError(info: '设置事件监听器失败: $e');
}
}
/// 初始化OpenAI服务
Future<bool> initialize() async {
try {
// 从.env文件中读取配置
// final apiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
// final baseUrl = dotenv.env['OPENAI_BASE_URL'] ?? '';
// final model = dotenv.env['OPENAI_MODEL'] ?? '';
// final _env = _storage.read("ENV") as Map<String, String>;
final apiKey = AppConfig.env('OPENAI_API_KEY') ?? '';
final baseUrl = AppConfig.env('OPENAI_BASE_URL') ?? '';
final model = AppConfig.env('OPENAI_MODEL') ?? '';
printInfo(info: '从.env读取OpenAI配置');
printInfo(info: '基础URL: $baseUrl');
printInfo(info: '模型名称: $model');
if (apiKey.isEmpty) {
printError(info: '错误: OpenAI API密钥未配置,请在.env文件中设置OPENAI_API_KEY');
return false;
}
final _mcps = _storage.read("MCPS") as Map<String, DBMCPServer>;
// 读取.mcp.json文件
String mcpConfig = jsonEncode(_mcps);
// try {
// // 从Flutter资源包中加载.mcp.json
// mcpConfig = await rootBundle.loadString('.mcp.json');
// printInfo(info: '成功从资源包加载.mcp.json配置文件');
// // 验证JSON格式
// final jsonData = jsonDecode(mcpConfig);
// if (jsonData is Map && jsonData.containsKey('mcpServers')) {
// printInfo(info: '解析到有效的mcpServers配置');
// } else {
// printInfo(info: '.mcp.json内容格式不正确,期望包含mcpServers字段');
// }
// } catch (e) {
// printError(info: '加载.mcp.json资源文件时出错: $e');
// }
// 初始化OpenAI服务
final result = await _openAIService.initialize(
apiKey: apiKey,
baseUrl: baseUrl,
model: model,
mcpServer: mcpConfig,
);
if (result) {
printInfo(info: 'OpenAI服务初始化成功');
} else {
printError(info: 'OpenAI服务初始化失败');
}
return result;
} catch (e) {
printError(info: 'OpenAI服务初始化异常: $e');
return false;
}
}
/// 发送消息并获取回复
@override
Future<String> sendMessage({
required List<Map<String, String>> messages,
String? systemPrompt,
String? userProperties,
}) async {
try {
// 在方法内直接转换
final convertedMessages =
messages.map((m) => Map<String, dynamic>.from(m)).toList();
// 发送消息并获取回复
final response = await _openAIService.sendMessage(
messages: convertedMessages,
systemPrompt: systemPrompt,
userProperties: userProperties,
);
return response;
} catch (e) {
printError(info: 'OpenAI发送消息失败: $e');
throw '发送消息失败: $e';
}
}
/// 发送消息并获取流式回复
@override
Stream sendMessageStream({
required List<Map<String, String>> messages,
String? systemPrompt, //提示词
String? userProperties, //用户属性
}) async* {
try {
// 在方法内直接转换
final convertedMessages =
messages.map((m) => Map<String, dynamic>.from(m)).toList();
// 创建用于接收token的控制器
final localController = StreamController();
// 添加从广播流到本地流的订阅
final subscription = _tokenStreamController.stream.listen((streamEvent) {
switch (streamEvent.type) {
case StreamEventType.token:
if (streamEvent.content != null && !localController.isClosed) {
localController.add(streamEvent.content!);
}
break;
case StreamEventType.functionCall:
if (streamEvent.meta != null && !localController.isClosed) {
localController.add(streamEvent.meta!);
}
break;
case StreamEventType.complete:
if (!localController.isClosed) {
localController.close();
}
break;
case StreamEventType.error:
if (!localController.isClosed) {
printError(info: '令牌流错误: ${streamEvent.content}');
localController.addError(streamEvent.content ?? '未知错误');
localController.close();
}
break;
}
});
// 当本地控制器关闭时,取消订阅
localController.onCancel = () {
subscription.cancel();
};
// 启动流式消息请求
bool started = false;
try {
started = await _openAIService.sendMessageStream(
messages: convertedMessages,
systemPrompt: systemPrompt,
userProperties: userProperties,
);
} catch (e) {
printError(info: '启动消息流失败: $e');
if (!localController.isClosed) {
localController.addError('启动消息流失败: $e');
localController.close();
}
throw '启动消息流失败: $e';
}
if (!started) {
printError(info: '无法启动消息流');
if (!localController.isClosed) {
localController.addError('无法启动消息流');
localController.close();
}
throw '无法启动消息流';
}
// 通过yield*将controller的流转发
yield* localController.stream;
} catch (e) {
printError(info: 'OpenAI流式消息处理失败: $e');
throw '流式消息处理失败: $e';
}
}
/// 注册函数
Future<bool> registerFunction(
String name, String description, Map<String, dynamic> parameters) async {
try {
final result = await _openAIService.registerFunction(
name: name,
description: description,
parameters: parameters,
);
if (result) {
printInfo(info: '函数 "$name" 注册成功');
} else {
printError(info: '函数 "$name" 注册失败');
}
return result;
} catch (e) {
printError(info: '注册函数失败: $e');
return false;
}
}
/// 释放资源
void dispose() {
try {
_eventSubscription?.cancel();
_tokenStreamController.close();
printInfo(info: 'OpenAIServiceAdapter资源已释放');
} catch (e) {
printError(info: '释放资源时出错: $e');
}
}
}

3
lib/data/services/speech_factory.dart

@ -6,6 +6,7 @@ import 'speech_impl/azure_tts_service.dart';
import 'speech_impl/flutter_asr_service.dart';
import 'speech_impl/flutter_tts_service.dart';
import '../../core/utils/logger.dart';
/// 语音服务类型
@ -15,6 +16,7 @@ enum SpeechServiceType {
/// Flutter 本地语音服务
flutter,
}
/// 语音服务工厂类
@ -61,6 +63,7 @@ class SpeechFactory extends GetxService {
Get.lazyPut<AsrService>(() => FlutterAsrService());
Get.lazyPut<TtsService>(() => FlutterTtsService());
break;
}
}

192
lib/data/services/speech_impl/azure_asr_service.dart

@ -1,81 +1,105 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:deep_voice/data/models/appconfig.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart';
import 'package:get/get.dart';
import '../asr_service.dart';
/// 音频源类型
enum AudioSourceType {
microphone, // 使用设备麦克风
external // 使用外部提供的音频数据
}
/// Azure 语音识别服务
///
/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口
///
/// 该服务提供了通过平台通道与原生 Microsoft Speech SDK 交互的接口
class AzureAsrService extends GetxService implements AsrService {
static final AzureAsrService to = Get.put(AzureAsrService());
static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_asr');
static const EventChannel _eventChannel = EventChannel('com.deep_voice.azure_asr_events');
static const MethodChannel _channel = MethodChannel('azure_speech/asr');
static const EventChannel _eventChannel =
EventChannel('azure_speech/asr_events');
// final GetStorage _storage = GetStorage();
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
final List<String> _defaultSupportedLanguages = ['zh-CN'];
final List<String> _defaultSupportedLanguages = ['zh-CN', 'en-US'];
@override
List<String> get supportedLanguages => _defaultSupportedLanguages;
// 连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 最新的识别结果
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
// 最新检测到的语言
String _latestDetectedLanguage = '';
String get latestDetectedLanguage => _latestDetectedLanguage;
// 当前音频源类型
AudioSourceType _audioSourceType = AudioSourceType.microphone;
AzureAsrService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
// final _env = _storage.read("ENV") as Map<String, String>;
_subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? '';
_serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? '';
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
throw Exception(
'未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
}
/// 设置事件通道
void _setupEventChannel() {
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen((event) {
if (event is Map) {
_handleRecognitionEvent(event);
}
}, onError: _handleRecognitionError);
_eventSubscription?.cancel();
_eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) {
if (event is Map) {
_handleRecognitionEvent(event);
}
}, onError: _handleRecognitionError);
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
bool useExternalAudio = false,
bool useEchoCancellation = false,
}) async {
try {
final List<String> languages = supportedLanguages ?? _defaultSupportedLanguages;
final List<String> languages =
supportedLanguages ?? _defaultSupportedLanguages;
// 检查是否需要重新初始化
if (_isInitialized) {
await dispose();
}
// 设置音频源类型
_audioSourceType = useExternalAudio
? AudioSourceType.external
: AudioSourceType.microphone;
final bool result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'supportedLanguages': languages,
'audioSourceType': _audioSourceType.toString().split('.').last,
'useEchoCancellation': useEchoCancellation,
});
_setupEventChannel();
_isInitialized = result;
@ -87,22 +111,27 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow;
}
}
@override
Future<RecognitionEvent> recognizeOnce() async {
if (!_isInitialized) {
await initialize();
}
try {
// 调用同步的recognizeOnce方法
final result = await _channel.invokeMethod('recognizeOnce');
if (result is Map) {
// 处理新格式返回
final String text = result['text'] as String? ?? '';
final String detectedLanguage = result['detectedLanguage'] as String? ?? '';
final String detectedLanguage =
result['detectedLanguage'] as String? ?? '';
// 更新最新识别结果
_latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage;
return RecognitionEvent.finalResult(
text: text,
detectedLanguage: detectedLanguage,
@ -112,35 +141,36 @@ class AzureAsrService extends GetxService implements AsrService {
_latestRecognizedText = result;
return RecognitionEvent.finalResult(text: result);
}
throw Exception('无效的识别结果格式');
} catch (e) {
Logger.error('语音识别失败: $e');
return RecognitionEvent.error(e.toString());
}
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) {
await initialize();
}
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
try {
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
// 开始连续识别
final bool result = await _channel.invokeMethod('startContinuousRecognition');
final bool result =
await _channel.invokeMethod('startContinuousRecognition');
if (!result) {
_cleanupEventStream();
throw Exception('启动连续识别失败');
}
_isContinuousRecognitionActive = true;
return _eventStreamController!.stream;
} catch (e) {
@ -149,15 +179,16 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow;
}
}
@override
Future<bool> stopContinuousRecognition() async {
if (!_isInitialized || !_isContinuousRecognitionActive) {
return true;
}
try {
final bool result = await _channel.invokeMethod('stopContinuousRecognition');
final bool result =
await _channel.invokeMethod('stopContinuousRecognition');
_cleanupEventStream();
return result;
} catch (e) {
@ -166,26 +197,27 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow;
}
}
@override
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
/// 处理来自原生端的识别事件
void _handleRecognitionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试
Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap');
switch (eventType) {
case 'result':
final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? '';
final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage;
_eventStreamController?.add(RecognitionEvent(
@ -194,45 +226,46 @@ class AzureAsrService extends GetxService implements AsrService {
detectedLanguage: detectedLanguage,
));
break;
case 'recognizing':
final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? '';
final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: text,
detectedLanguage: detectedLanguage,
));
break;
case 'sessionStarted':
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted,
));
break;
case 'sessionStopped':
_isContinuousRecognitionActive = false;
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped,
));
break;
case 'canceled':
_isContinuousRecognitionActive = false;
final String reason = eventMap['reason'] as String? ?? '';
final String errorDetails = eventMap['errorDetails'] as String? ?? '';
if (reason.isNotEmpty || errorDetails.isNotEmpty) {
Logger.error('识别取消: $reason - $errorDetails');
}
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.canceled,
error: '$reason: $errorDetails',
));
break;
case 'error':
final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: $error');
@ -243,36 +276,36 @@ class AzureAsrService extends GetxService implements AsrService {
break;
}
}
/// 处理识别事件流错误
void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: $error');
_eventStreamController?.addError(error);
_cleanupEventStream();
}
/// 清理事件流资源
void _cleanupEventStream() {
_eventStreamController?.close();
_eventStreamController = null;
_isContinuousRecognitionActive = false;
}
@override
Future<void> dispose() async {
try {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
// 取消事件订阅
await _eventSubscription?.cancel();
_eventSubscription = null;
// 通知原生端释放资源
await _channel.invokeMethod('dispose');
_isInitialized = false;
Logger.info('Azure 语音识别资源已释放');
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
@ -280,4 +313,31 @@ class AzureAsrService extends GetxService implements AsrService {
_isInitialized = false;
}
}
}
@override
Future<bool> pushAudioData(Uint8List data) async {
if (!_isInitialized) {
await initialize(useExternalAudio: true);
}
if (_audioSourceType != AudioSourceType.external) {
Logger.error('当前非外部音频模式,不能推送音频数据');
return false;
}
try {
final bool result = await _channel.invokeMethod('pushAudioData', {
'audioData': data // Flutter 会自动将 Uint8List 转换为 ByteBuffer
});
if (!result) {
throw Exception('推送音频数据失败');
}
return true;
} catch (e) {
Logger.error('推送音频数据失败: $e');
return false;
}
}
}

515
lib/data/services/speech_impl/azure_tts_service.dart

@ -1,134 +1,179 @@
import 'dart:async';
import 'dart:io';
import 'package:deep_voice/data/models/appconfig.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart';
import '../tts_service.dart';
/// 微软 Text-to-Speech 服务
///
///
/// 该服务通过平台通道与原生 Microsoft Speech SDK 交互,
/// 提供文本转语音功能。
class AzureTtsService extends GetxService implements TtsService {
static final AzureTtsService to = Get.put(AzureTtsService());
static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_tts');
static const MethodChannel _channel = MethodChannel('azure_speech/tts');
static const EventChannel _eventChannel =
EventChannel('azure_speech/tts_events');
// final GetStorage _storage = GetStorage();
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
// 当前设置
String _currentVoice = 'zh-CN-XiaoxiaoNeural';
int _currentRate = 0;
int _currentPitch = 0;
int _currentVolume = 100;
// 支持的语音列表
final List<String> _supportedVoices = [
'zh-CN-XiaoxiaoNeural',
'zh-CN-YunxiNeural',
'zh-CN-YunjianNeural',
'en-US-JennyNeural',
'en-US-GuyNeural'
];
// 语音合成队列
final List<_SpeechItem> _textQueue = [];
final List<String> _textQueue = [];
bool _isProcessingQueue = false;
// 事件流
final StreamController<TtsEvent> _eventStreamController =
StreamController<TtsEvent>.broadcast();
StreamSubscription? _eventSubscription;
// 可观察状态
final isEnabled = true.obs;
final _isSpeaking = false.obs;
// 流式文本缓冲区
String _streamBuffer = '';
/// 缓存的播放状态
bool _speakingCache = false;
DateTime _lastSpeakingCheck = DateTime.now();
@override
String get currentVoice => _currentVoice;
@override
bool get isSpeaking {
// 如果未初始化,直接返回false
if (!_isInitialized) return false;
// 如果距离上次检查不到50ms,则使用缓存值
if (DateTime.now().difference(_lastSpeakingCheck).inMilliseconds < 50) {
return _speakingCache;
}
// 否则异步更新缓存(但仍返回当前缓存值)
_checkSpeakingStatus();
return _speakingCache;
}
@override
Stream<TtsEvent> get onEvent => const Stream.empty();
bool get isSpeaking => _isSpeaking.value;
@override
List<String> get supportedVoices => _supportedVoices;
Stream<TtsEvent> get onEvent => _eventStreamController.stream;
AzureTtsService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
// final _env = _storage.read("ENV") as Map<String, String>;
_subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? '';
_serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? '';
}
/// 设置事件通道
void _setupEventChannel() {
_eventSubscription?.cancel();
_eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) {
if (event is Map) {
_handleTtsEvent(event);
}
}, onError: (error) {
Logger.error('TTS事件流错误: $error');
_eventStreamController.addError(error);
});
}
/// 处理TTS事件
void _handleTtsEvent(Map<dynamic, dynamic> event) {
final String eventType = event['type'] as String? ?? '';
// Logger.debug('收到TTS事件: $eventType, 数据: $event');
switch (eventType) {
case 'synthesis_started':
_isSpeaking.value = true;
_eventStreamController.add(TtsEvent(type: TtsEventType.started));
break;
case 'synthesizing':
// _isSpeaking.value = true;
// // 合成中事件暂时映射到started类型,因为TtsEventType中没有synthesizing类型
// _eventStreamController.add(TtsEvent(type: TtsEventType.started));
break;
case 'synthesis_completed':
_isSpeaking.value = false;
_eventStreamController.add(TtsEvent(type: TtsEventType.completed));
// 如果有队列在处理中,继续处理下一项
if (_isProcessingQueue) {
_processNextInQueue();
}
break;
case 'synthesis_canceled':
_isSpeaking.value = false;
final String reason = event['reason'] as String? ?? '';
_eventStreamController.add(TtsEvent.error('合成取消: $reason'));
// 如果有队列在处理中,继续处理下一项(即使出错)
if (_isProcessingQueue) {
_processNextInQueue();
}
break;
case 'error':
_isSpeaking.value = false;
final String message = event['message'] as String? ?? '未知错误';
Logger.error('TTS错误: $message');
_eventStreamController.add(TtsEvent.error(message));
// 如果有队列在处理中,继续处理下一项(即使出错)
if (_isProcessingQueue) {
_processNextInQueue();
}
break;
}
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
// 检查是否需要重新初始化
if (_isInitialized) {
await dispose();
}
// 检查是否需要重新初始化
if (_isInitialized) {
await dispose();
}
try {
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
Logger.error('未找到Azure语音服务配置');
return false;
}
// 先设置事件通道,确保在调用initialize方法前已准备好
_setupEventChannel();
// 更新支持的语音列表(如果提供)
final String language = supportedLanguages?.first ?? 'zh-CN';
final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'language': language,
});
_isInitialized = true;
return result;
final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'language': language,
});
_isInitialized = result;
return result;
} catch (e) {
Logger.error('初始化失败: $e');
return false;
}
}
@override
Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize();
if (voiceName == _currentVoice) return true;
try {
final result = await _channel.invokeMethod('setVoice', {
'voiceName': voiceName,
});
if (result) _currentVoice = voiceName;
return result;
} catch (e) {
@ -136,122 +181,48 @@ class AzureTtsService extends GetxService implements TtsService {
return false;
}
}
/// 设置语音参数
Future<bool> setSpeechParams({int rate = 0, int pitch = 0, int volume = 100}) async {
if (!_isInitialized) await initialize();
try {
final result = await _channel.invokeMethod('setSpeechParams', {
'rate': rate,
'pitch': pitch,
'volume': volume,
});
if (result) {
_currentRate = rate;
_currentPitch = pitch;
_currentVolume = volume;
}
return result;
} catch (e) {
Logger.error('设置语音参数失败: $e');
return false;
}
}
@override
Future<bool> speakOnce(String text) async {
if (!_isInitialized) await initialize();
if (!isEnabled.value || text.isEmpty) return false;
try {
// 主动设置缓存状态为true
_speakingCache = true;
final result = await _channel.invokeMethod('speakText', {'text': text});
// 调用完成后主动检查状态
_checkSpeakingStatus();
return result == "OK";
} catch (e) {
// 发生错误时重置状态
_speakingCache = false;
Logger.error('1语音合成失败: $e');
return false;
}
}
/// 播放SSML
Future<bool> speakSsmlOnce(String ssml) async {
if (!_isInitialized) await initialize();
if (!isEnabled.value || ssml.isEmpty) return false;
try {
// 主动设置缓存状态为true
_speakingCache = true;
final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml});
// 调用完成后主动检查状态
_checkSpeakingStatus();
return result == "OK";
// 开始合成,传递参数
final result = await _channel.invokeMethod('speakText', {
'text': text,
});
return result == true;
} catch (e) {
// 发生错误时重置状态
_speakingCache = false;
Logger.error('SSML语音合成失败: $e');
Logger.error('语音合成失败: $e');
return false;
}
}
/// 等待语音播放完成
Future<void> waitForSpeechCompletion({Duration timeout = const Duration(seconds: 30)}) async {
// 首先检查当前状态
_checkSpeakingStatus();
if (!_speakingCache) return;
final stopwatch = Stopwatch()..start();
while (_speakingCache && stopwatch.elapsed < timeout) {
// 短暂等待后再次检查
await Future.delayed(const Duration(milliseconds: 100));
// 检查状态
await _channel.invokeMethod<bool>('isSpeaking').then((result) {
if (result != null) {
_speakingCache = result;
_lastSpeakingCheck = DateTime.now();
}
});
}
}
@override
Future<bool> speakStream(String text) async {
if (!isEnabled.value || text.isEmpty) return false;
try {
// 添加文本到缓冲区
_streamBuffer += text;
// 如果缓冲区为空,直接返回
if (_streamBuffer.isEmpty) return true;
// 使用正则表达式匹配句子,包括结束符号
// 匹配任意字符,直到遇到句子结束符号
final sentenceRegex = RegExp(
r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])'
);
r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』"])');
bool hasProcessed = false;
// 查找所有完整句子
final matches = sentenceRegex.allMatches(_streamBuffer);
final List<String> sentences = [];
int lastMatchEnd = 0;
for (final match in matches) {
// 提取完整句子(包含结束符号)
final sentence = match.group(1)?.trim();
@ -260,208 +231,186 @@ class AzureTtsService extends GetxService implements TtsService {
lastMatchEnd = match.end;
}
}
// 处理找到的句子
for (final sentence in sentences) {
_textQueue.add(_SpeechItem(
text: sentence,
rate: _currentRate,
pitch: _currentPitch,
volume: _currentVolume,
));
_textQueue.add(sentence);
hasProcessed = true;
}
// 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd);
}
// 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) {
_processQueue();
}
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
return false;
}
}
@override
Future<bool> flushStream() async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try {
// 处理缓冲区中可能的完整句子
await speakStream('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_SpeechItem(
text: _streamBuffer,
rate: _currentRate,
pitch: _currentPitch,
volume: _currentVolume,
));
_textQueue.add(_streamBuffer);
// 清空缓冲区
_streamBuffer = '';
// 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) {
_processQueue();
}
}
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
// 等待队列处理完成
await _checkSpeakingStatus();
while (_isProcessingQueue || _speakingCache) {
await Future.delayed(const Duration(milliseconds: 100));
await _checkSpeakingStatus();
final completer = Completer<bool>();
// 监听事件流检查队列是否完成
final subscription = onEvent.listen((event) {
// 当队列为空且不在处理中,且当前非说话状态,完成操作
if (_textQueue.isEmpty &&
!_isProcessingQueue &&
!_isSpeaking.value &&
!completer.isCompleted) {
completer.complete(true);
}
});
// 如果已经处于空闲状态,直接完成
if (_textQueue.isEmpty && !_isProcessingQueue && !_isSpeaking.value) {
subscription.cancel();
return true;
}
// 设置超时
final timeout = Timer(const Duration(seconds: 60), () {
if (!completer.isCompleted) {
Logger.warning('等待TTS队列完成超时');
completer.complete(false);
}
});
// 等待完成
final result = await completer.future;
// 清理
subscription.cancel();
timeout.cancel();
Logger.info('TTS队列播放完成');
return true;
return result;
} catch (e) {
Logger.error('刷新TTS流失败: $e');
return false;
}
}
/// 处理语音合成队列
Future<void> _processQueue() async {
if (_textQueue.isEmpty || _isProcessingQueue) return;
_isProcessingQueue = true;
// 处理队列中的第一项
await _processNextInQueue();
}
/// 处理队列中的下一项
Future<void> _processNextInQueue() async {
// 检查队列是否为空或已禁用
if (_textQueue.isEmpty || !isEnabled.value) {
_textQueue.clear();
_isProcessingQueue = false;
return;
}
try {
while (_textQueue.isNotEmpty) {
if (!isEnabled.value) {
_textQueue.clear();
break;
}
final item = _textQueue.removeAt(0);
if (item.rate != _currentRate || item.pitch != _currentPitch || item.volume != _currentVolume) {
await setSpeechParams(
rate: item.rate,
pitch: item.pitch,
volume: item.volume,
);
}
// 使用speakOnce播放当前项
await speakOnce(item.text);
// 等待播放完成
await waitForSpeechCompletion();
// 获取当前项但暂不从队列中移除
final text = _textQueue.first;
// 开始合成
final success = await speakOnce(text);
// 仅当合成操作失败时才从队列移除并继续处理下一项
// 成功的话会等待合成完成事件后自动处理下一项
if (!success) {
_textQueue.removeAt(0);
await _processNextInQueue();
} else {
// 成功启动合成,从队列中移除当前项
// 注意:下一项的处理将在合成完成事件中触发
_textQueue.removeAt(0);
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
} finally {
_isProcessingQueue = false;
// 出错时移除当前项并尝试继续
if (_textQueue.isNotEmpty) {
_textQueue.removeAt(0);
}
// 如果队列中还有项目,继续处理
if (_textQueue.isNotEmpty) {
await _processNextInQueue();
} else {
_isProcessingQueue = false;
}
}
}
@override
Future<bool> stop() async {
try {
_textQueue.clear();
if (_speakingCache) {
if (_isSpeaking.value) {
final result = await _channel.invokeMethod('stopSpeaking');
// 重置缓存状态
_speakingCache = false;
_isSpeaking.value = false;
_isProcessingQueue = false;
return result;
}
return true;
} catch (e) {
Logger.error('停止语音合成出错: $e');
return false;
}
}
/// 生成SSML文本
String generateSsml({
required String text,
String? voiceName,
int? rate,
int? pitch,
int? volume,
}) {
final voice = voiceName ?? _currentVoice;
final rateValue = (rate ?? _currentRate).clamp(-100, 100);
final pitchValue = (pitch ?? _currentPitch).clamp(-100, 100);
final volumeValue = (volume ?? _currentVolume).clamp(0, 100);
final rateStr = rateValue == 0 ? '0%' : rateValue < 0 ? '${(rateValue * 0.9).round()}%' : '$rateValue%';
final pitchStr = pitchValue == 0 ? '0%' : '${(pitchValue * 0.5).round()}%';
final volumeStr = "$volumeValue%";
return '''
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="$voice">
<prosody rate="$rateStr" pitch="$pitchStr" volume="$volumeStr">
$text
</prosody>
</voice>
</speak>
''';
}
/// 切换启用状态
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) stop();
}
@override
Future<void> dispose() async {
if (!_isInitialized) return;
try {
await stop();
await _eventSubscription?.cancel();
await _channel.invokeMethod('dispose');
_isInitialized = false;
} catch (e) {
Logger.error('释放资源失败: $e');
}
}
/// 异步检查并更新播放状态缓存
Future<bool> _checkSpeakingStatus() async {
_lastSpeakingCheck = DateTime.now();
try {
final result = await _channel.invokeMethod<bool>('isSpeaking');
if (result != null) {
_speakingCache = result;
}
return _speakingCache;
} catch (e) {
Logger.error('获取播放状态失败: $e');
return _speakingCache;
}
}
}
/// 语音合成项目
class _SpeechItem {
final String text;
final int rate;
final int pitch;
final int volume;
_SpeechItem({
required this.text,
this.rate = 0,
this.pitch = 0,
this.volume = 100,
});
}

236
lib/data/services/speech_impl/flutter_asr_service.dart

@ -11,26 +11,26 @@ import '../asr_service.dart';
enum AsrState {
/// 未初始化
notInitialized,
/// 已初始化但未开始监听
initialized,
/// 正在监听
listening,
/// 正在处理识别结果
processing,
/// 识别完成
done,
/// 发生错误
error,
}
/// Flutter ASR服务,负责本地语音转文本功能
/// 使用speech_to_text插件实现语音识别功能
class FlutterAsrService extends GetxService implements AsrService {
class FlutterAsrService extends GetxService implements AsrService {
static final FlutterAsrService to = Get.put(FlutterAsrService());
// speech_to_text 实例
@ -40,34 +40,34 @@ class FlutterAsrService extends GetxService implements AsrService {
final List<String> _supportedLanguages = [];
@override
List<String> get supportedLanguages => _supportedLanguages;
// 当前选择的语言
final RxString _currentLocale = ''.obs;
String get currentLocale => _currentLocale.value;
// 是否可用
final RxBool _isAvailable = false.obs;
bool get isAvailable => _isAvailable.value;
// 是否正在监听
bool _isListening = false;
// 当前ASR状态
final Rx<AsrState> _asrState = AsrState.notInitialized.obs;
AsrState get asrState => _asrState.value;
// 连续识别模式的结果流控制器
StreamController<RecognitionEvent>? _continuousRecognitionController;
FlutterAsrService() {
_initSpeech();
}
/// 初始化语音识别
Future<void> _initSpeech() async {
try {
_asrState.value = AsrState.notInitialized;
// 初始化语音识别,使用较短的最终超时时间,提高响应速度
_isAvailable.value = await _speech.initialize(
onStatus: _onSpeechStatus,
@ -75,11 +75,11 @@ class FlutterAsrService extends GetxService implements AsrService {
debugLogging: false,
finalTimeout: const Duration(milliseconds: 800), // 更短的最终超时时间,加速结果返回
);
if (_isAvailable.value) {
// 获取支持的语言列表
final systemLocales = await _speech.locales();
// 清空并重新填充支持的语言列表
_supportedLanguages.clear();
for (var locale in systemLocales) {
@ -87,7 +87,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_supportedLanguages.add(locale.localeId);
}
}
// 设置默认语言(优先使用系统语言或中文)
var systemLocale = await _speech.systemLocale();
if (systemLocale != null) {
@ -97,7 +97,7 @@ class FlutterAsrService extends GetxService implements AsrService {
} else if (_supportedLanguages.isNotEmpty) {
_currentLocale.value = _supportedLanguages.first;
}
_asrState.value = AsrState.initialized;
Logger.info('语音识别初始化成功,支持 ${_supportedLanguages.length} 种语言');
} else {
@ -110,11 +110,11 @@ class FlutterAsrService extends GetxService implements AsrService {
Logger.error('语音识别初始化失败: $e');
}
}
/// 处理语音识别状态变化
void _onSpeechStatus(String status) {
Logger.info('语音识别状态: $status');
switch (status) {
case 'listening':
_asrState.value = AsrState.listening;
@ -129,31 +129,33 @@ class FlutterAsrService extends GetxService implements AsrService {
case 'done':
_asrState.value = AsrState.done;
_isListening = false;
// 在连续识别模式下,当一次识别完成后立即开始下一次识别
if (_continuousRecognitionController != null && !_continuousRecognitionController!.isClosed) {
if (_continuousRecognitionController != null &&
!_continuousRecognitionController!.isClosed) {
_restartContinuousRecognition();
}
break;
}
}
/// 处理语音识别错误
void _onSpeechError(SpeechRecognitionError error) {
Logger.error('语音识别错误: ${error.errorMsg} (永久性错误: ${error.permanent})');
_asrState.value = AsrState.error;
// 向连续识别控制器发送错误事件(如果存在)
_continuousRecognitionController?.add(
RecognitionEvent.error(error.errorMsg)
);
_continuousRecognitionController
?.add(RecognitionEvent.error(error.errorMsg));
// 在连续识别模式下,尝试从错误中恢复(非永久性错误)
if (!error.permanent && _continuousRecognitionController != null && !_continuousRecognitionController!.isClosed) {
if (!error.permanent &&
_continuousRecognitionController != null &&
!_continuousRecognitionController!.isClosed) {
_restartContinuousRecognition();
}
}
/// 启动语音识别的统一方法
Future<bool> _startListening({
required Function(SpeechRecognitionResult) onResult,
@ -168,36 +170,37 @@ class FlutterAsrService extends GetxService implements AsrService {
// 延长等待时间,确保资源完全释放
await Future.delayed(const Duration(milliseconds: 800));
}
// 在启动前再次检查状态
if (_speech.isListening) {
Logger.warning('停止识别后,仍处于监听状态,可能有系统冲突');
await _speech.stop();
await Future.delayed(const Duration(milliseconds: 500));
}
// 启动语音识别前确保状态正确
_asrState.value = AsrState.listening;
// 启动语音识别
final result = await _speech.listen(
onResult: onResult,
localeId: _currentLocale.value,
listenFor: listenFor,
pauseFor: pauseFor,
listenOptions: stt.SpeechListenOptions(
partialResults: true,
autoPunctuation: autoPunctuation,
// 使用独白模式,减少与系统交互冲突
listenMode: stt.ListenMode.dictation,
),
) ?? false;
onResult: onResult,
localeId: _currentLocale.value,
listenFor: listenFor,
pauseFor: pauseFor,
listenOptions: stt.SpeechListenOptions(
partialResults: true,
autoPunctuation: autoPunctuation,
// 使用独白模式,减少与系统交互冲突
listenMode: stt.ListenMode.dictation,
),
) ??
false;
if (!result) {
Logger.error('语音识别启动失败');
_asrState.value = AsrState.error;
}
return result;
} catch (e) {
Logger.error('启动语音识别失败: $e');
@ -205,33 +208,35 @@ class FlutterAsrService extends GetxService implements AsrService {
return false;
}
}
/// 重新启动连续识别
Future<void> _restartContinuousRecognition() async {
try {
// 确保控制器有效
if (_continuousRecognitionController == null || _continuousRecognitionController!.isClosed) {
if (_continuousRecognitionController == null ||
_continuousRecognitionController!.isClosed) {
Logger.info('连续识别控制器已关闭,跳过重启');
return;
}
// 检查系统状态,避免频繁重启
if (_speech.isListening) {
Logger.info('当前已在监听中,无需重启');
return;
}
// 延长等待时间,避免与系统资源冲突
await Future.delayed(const Duration(milliseconds: 1000));
// 再次检查状态
if (_continuousRecognitionController == null || _continuousRecognitionController!.isClosed) {
if (_continuousRecognitionController == null ||
_continuousRecognitionController!.isClosed) {
Logger.info('延迟期间控制器已关闭,跳过重启');
return;
}
Logger.info('尝试重新启动连续识别');
// 使用统一方法启动识别
final success = await _startListening(
onResult: _onSpeechResult,
@ -239,49 +244,42 @@ class FlutterAsrService extends GetxService implements AsrService {
listenFor: const Duration(seconds: 30),
pauseFor: const Duration(seconds: 4),
);
if (success) {
Logger.info('连续识别重启成功');
} else {
Logger.error('连续识别重启失败');
_continuousRecognitionController?.add(
RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突')
);
_continuousRecognitionController
?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突'));
}
} catch (e) {
Logger.error('重启连续识别失败: $e');
// 发送错误给控制器
_continuousRecognitionController?.add(
RecognitionEvent.error('重启失败: $e')
);
_continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e'));
}
}
/// 处理语音识别结果
void _onSpeechResult(SpeechRecognitionResult result) {
// 如果是连续识别,发送到连续识别控制器
if (_continuousRecognitionController != null) {
if (result.finalResult) {
Logger.info('收到最终识别结果: "${result.recognizedWords}"');
_continuousRecognitionController!.add(
RecognitionEvent.finalResult(
text: result.recognizedWords,
detectedLanguage: currentLocale,
)
);
_continuousRecognitionController!.add(RecognitionEvent.finalResult(
text: result.recognizedWords,
detectedLanguage: currentLocale,
));
} else {
_continuousRecognitionController!.add(
RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: result.recognizedWords,
detectedLanguage: currentLocale,
)
);
_continuousRecognitionController!.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: result.recognizedWords,
detectedLanguage: currentLocale,
));
}
}
}
/// 设置当前语言
void setLocale(String localeId) {
if (_supportedLanguages.contains(localeId)) {
@ -291,7 +289,7 @@ class FlutterAsrService extends GetxService implements AsrService {
Logger.error('不支持的语言: $localeId');
}
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
@ -301,7 +299,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_supportedLanguages.clear();
_supportedLanguages.addAll(supportedLanguages);
}
// 初始化语音识别
await _initSpeech();
return _isAvailable.value;
@ -310,24 +308,24 @@ class FlutterAsrService extends GetxService implements AsrService {
return false;
}
}
@override
Future<RecognitionEvent> recognizeOnce() async {
try {
if (!_isAvailable.value) {
return RecognitionEvent.error('语音识别不可用');
}
if (_isListening) {
await stopContinuousRecognition();
}
// 创建Completer来等待最终结果
final completer = Completer<RecognitionEvent>();
// 开始监听
_asrState.value = AsrState.listening;
// 定义结果处理函数
void resultListener(SpeechRecognitionResult result) {
if (result.finalResult) {
@ -338,21 +336,21 @@ class FlutterAsrService extends GetxService implements AsrService {
));
}
}
// 使用统一方法启动识别,单次识别使用更短的时间参数
final success = await _startListening(
onResult: resultListener,
listenFor: const Duration(seconds: 15),
pauseFor: const Duration(seconds: 2),
);
if (!success) {
return RecognitionEvent.error('启动语音识别失败');
}
// 设置较短的超时时间
return await completer.future.timeout(
const Duration(seconds: 20), // 略长于listenFor以确保获取到结果
const Duration(seconds: 20), // 略长于listenFor以确保获取到结果
onTimeout: () {
_speech.stop();
return RecognitionEvent.error('语音识别超时');
@ -363,7 +361,7 @@ class FlutterAsrService extends GetxService implements AsrService {
return RecognitionEvent.error('$e');
}
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
try {
@ -373,43 +371,42 @@ class FlutterAsrService extends GetxService implements AsrService {
controller.close();
return controller.stream;
}
// 如果已经在监听,先停止
if (_isListening) {
await stopContinuousRecognition();
}
// 创建新的流控制器
_continuousRecognitionController = StreamController<RecognitionEvent>.broadcast();
_continuousRecognitionController =
StreamController<RecognitionEvent>.broadcast();
// 开始会话
_continuousRecognitionController!.add(
RecognitionEvent(type: RecognitionEventType.sessionStarted)
);
_continuousRecognitionController!
.add(RecognitionEvent(type: RecognitionEventType.sessionStarted));
// 开始连续监听
_asrState.value = AsrState.listening;
// 使用统一方法启动识别,连续识别使用较长的参数
final success = await _startListening(
onResult: _onSpeechResult,
);
if (!success) {
// 添加错误事件,但保留流
_continuousRecognitionController!.add(
RecognitionEvent.error('启动连续语音识别失败')
);
_continuousRecognitionController!
.add(RecognitionEvent.error('启动连续语音识别失败'));
// 返回已有的流控制器
return _continuousRecognitionController!.stream;
}
_isListening = true;
return _continuousRecognitionController!.stream;
} catch (e) {
Logger.error('启动连续语音识别失败: $e');
// 如果已经创建了控制器,使用它
if (_continuousRecognitionController != null) {
_continuousRecognitionController!.add(RecognitionEvent.error('$e'));
@ -422,7 +419,7 @@ class FlutterAsrService extends GetxService implements AsrService {
}
}
}
@override
Future<bool> stopContinuousRecognition() async {
try {
@ -430,36 +427,35 @@ class FlutterAsrService extends GetxService implements AsrService {
if (_speech.isListening) {
await _speech.stop();
}
_isListening = false;
// 发送会话结束事件
_continuousRecognitionController?.add(
RecognitionEvent(type: RecognitionEventType.sessionStopped)
);
_continuousRecognitionController
?.add(RecognitionEvent(type: RecognitionEventType.sessionStopped));
// 关闭流控制器
await _continuousRecognitionController?.close();
_continuousRecognitionController = null;
_asrState.value = AsrState.done;
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
// 强制重置状态
_isListening = false;
_asrState.value = AsrState.error;
return false;
}
}
@override
bool isContinuousRecognitionActive() {
return _isListening;
}
@override
Future<void> dispose() async {
try {
@ -467,15 +463,15 @@ class FlutterAsrService extends GetxService implements AsrService {
if (_speech.isListening) {
await _speech.stop();
}
// 关闭控制器
await _continuousRecognitionController?.close();
_continuousRecognitionController = null;
_isListening = false;
_asrState.value = AsrState.notInitialized;
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
}
}
}
}

347
lib/data/services/speech_impl/volcano_asr_service.dart

@ -0,0 +1,347 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../asr_service.dart';
/// 火山语音识别服务实现
class VolcanoAsrService extends GetxService implements AsrService {
/// 方法通道
static const MethodChannel _channel = MethodChannel('volcano_speech/asr');
/// 事件通道
static const EventChannel _eventChannel =
EventChannel('volcano_speech/asr_events');
/// 事件流控制器
final StreamController<RecognitionEvent> _eventStreamController =
StreamController<RecognitionEvent>.broadcast();
/// 事件订阅
StreamSubscription? _eventSubscription;
/// 是否初始化完成
bool _isInitialized = false;
/// 是否正在连续识别
bool _isContinuousRecognitionActive = false;
/// 当前语言
String _currentLanguage = 'zh-CN';
/// 支持的语言列表
final List<String> _supportedLanguages = ['zh-CN', 'en-US'];
@override
List<String> get supportedLanguages => _supportedLanguages;
/// 构造函数
VolcanoAsrService() {
_setupEventListener();
}
/// 初始化语音识别服务
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
if (_isInitialized) return true;
try {
// 如果提供了支持的语言列表,则替换默认列表
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_supportedLanguages.clear();
_supportedLanguages.addAll(supportedLanguages);
}
// 从.env文件读取配置
final appId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? '';
final appToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
final resourceId =
dotenv.env['VOLCANO_ASR_RESOURCE_ID'] ?? 'volc.bigasr.sauc.duration';
if (appId.isEmpty || appToken.isEmpty) {
debugPrint(
'火山语音初始化失败:缺少VOLCANO_SPEECH_APP_ID或VOLCANO_SPEECH_APP_TOKEN环境变量');
return false;
}
// 初始化火山语音ASR
final initResult = await _channel.invokeMethod<bool>('initialize', {
'appId': appId,
'appToken': appToken,
'resourceId': resourceId,
}) ??
false;
if (!initResult) {
debugPrint('火山语音ASR初始化失败');
return false;
}
// 设置默认语言、VAD参数和启用音量返回
await _configureAsr();
_isInitialized = true;
return true;
} catch (e) {
debugPrint('火山语音ASR初始化异常:$e');
return false;
}
}
/// 配置ASR参数
Future<void> _configureAsr() async {
// 设置默认语言
await _channel.invokeMethod<bool>('setLanguage', {
'language': _currentLanguage,
});
}
/// 设置事件监听
void _setupEventListener() {
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(
(dynamic event) {
if (event is! Map) return;
_handleAsrEvent(Map<String, dynamic>.from(event as Map));
},
onError: (Object error) {
debugPrint('ASR事件流错误: $error');
_eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
error: '事件流错误: $error',
));
},
);
}
/// 处理ASR事件
void _handleAsrEvent(Map<String, dynamic> event) {
final eventType = event['type'] as String?;
switch (eventType) {
case 'sessionStarted':
_eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted,
));
break;
case 'sessionStopped':
_eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped,
));
_isContinuousRecognitionActive = false;
break;
case 'recognizing':
final text = event['text'] as String?;
final language = event['language'] as String?;
if (text != null) {
debugPrint('接收到中间识别结果: $text');
_eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: text,
detectedLanguage: language ?? _currentLanguage,
));
}
break;
case 'result':
final text = event['text'] as String?;
final language = event['language'] as String?;
if (text != null) {
debugPrint('接收到最终识别结果: $text');
_eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.finalResult,
text: text,
detectedLanguage: language ?? _currentLanguage,
));
}
break;
case 'error':
final errorMessage = event['message'] as String?;
debugPrint('接收到识别错误: ${errorMessage ?? "未知错误"}');
_eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
error: errorMessage ?? '未知错误',
));
_isContinuousRecognitionActive = false;
break;
case 'volumeChanged':
// 音量变化事件仅在日志中输出,不发送到事件流
final volume = event['volume'] as int?;
if (volume != null && volume > 5) {
// 仅记录有意义的音量变化
debugPrint('音量变化: $volume');
}
break;
}
}
/// 检查初始化状态
Future<bool> _ensureInitialized() async {
if (!_isInitialized) {
return await initialize();
}
return true;
}
/// 执行一次性语音识别
@override
Future<RecognitionEvent> recognizeOnce() async {
if (!await _ensureInitialized()) {
return RecognitionEvent(
type: RecognitionEventType.error,
error: '语音识别服务未初始化',
);
}
try {
// 创建一个Completer用于等待最终结果
final completer = Completer<RecognitionEvent>();
// 创建一次性事件监听
StreamSubscription? subscription;
subscription = _eventStreamController.stream.listen((event) {
// 仅处理finalResult和error事件
if (event.type == RecognitionEventType.finalResult) {
// 识别成功,返回结果
completer.complete(event);
subscription?.cancel();
} else if (event.type == RecognitionEventType.error) {
// 识别失败,返回错误
completer.complete(event);
subscription?.cancel();
}
});
// 启动一次性识别
final started =
await _channel.invokeMethod<bool>('recognizeOnce') ?? false;
if (!started) {
subscription.cancel();
return RecognitionEvent(
type: RecognitionEventType.error,
error: '启动一次性识别失败',
);
}
// 设置超时,防止永久等待
Future.delayed(const Duration(seconds: 30), () {
if (!completer.isCompleted) {
completer.complete(RecognitionEvent(
type: RecognitionEventType.error,
error: '识别超时',
));
subscription?.cancel();
// 停止识别
_channel.invokeMethod('stopRecognize');
}
});
// 等待结果
return await completer.future;
} catch (e) {
// 确保停止识别
try {
await _channel.invokeMethod('stopRecognize');
} catch (_) {}
return RecognitionEvent(
type: RecognitionEventType.error,
error: e.toString(),
);
}
}
/// 开始连续语音识别
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!await _ensureInitialized()) {
_eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
error: '语音识别服务未初始化',
));
return _eventStreamController.stream;
}
try {
final success =
await _channel.invokeMethod<bool>('startContinuousRecognition') ??
false;
if (success) {
_isContinuousRecognitionActive = true;
} else {
_eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
error: '启动连续识别失败',
));
}
} catch (e) {
_eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
error: '启动连续识别异常:$e',
));
}
return _eventStreamController.stream;
}
/// 停止连续语音识别
@override
Future<bool> stopContinuousRecognition() async {
if (!_isInitialized || !_isContinuousRecognitionActive) {
return false;
}
try {
final success =
await _channel.invokeMethod<bool>('stopContinuousRecognition') ??
false;
if (success) {
_isContinuousRecognitionActive = false;
}
return success;
} catch (e) {
debugPrint('停止连续识别异常:$e');
return false;
}
}
/// 检查连续识别是否活跃
@override
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
/// 释放资源
@override
Future<void> dispose() async {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
await _eventSubscription?.cancel();
_eventSubscription = null;
if (!_eventStreamController.isClosed) {
await _eventStreamController.close();
}
if (_isInitialized) {
await _channel.invokeMethod<bool>('release');
_isInitialized = false;
}
}
}

285
lib/data/services/speech_impl/volcano_tts_service.dart

@ -0,0 +1,285 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../tts_service.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// 火山语音合成服务实现
class VolcanoTtsService extends GetxService implements TtsService {
/// 方法通道
static const MethodChannel _channel = MethodChannel('volcano_speech/tts');
/// 事件通道
static const EventChannel _eventChannel = EventChannel('volcano_speech/tts_events');
/// 音频数据通道
static const EventChannel _audioDataChannel = EventChannel('volcano_speech/tts_audio_data');
/// 事件流控制器
final StreamController<TtsEvent> _eventStreamController =
StreamController<TtsEvent>.broadcast();
/// 音频数据流控制器
final StreamController<Uint8List> _audioDataStreamController =
StreamController<Uint8List>.broadcast();
/// 事件处理器
StreamSubscription? _eventSubscription;
/// 音频数据处理器
StreamSubscription? _audioDataSubscription;
/// 是否初始化完成
bool _isInitialized = false;
/// 当前语音名称
String _currentVoice = "zh_female_qingxin";
/// 是否启用音频回调
bool _isAudioCallbackEnabled = false;
@override
String get currentVoice => _currentVoice;
@override
bool get isSpeaking => _isSpeaking;
/// 是否正在播放
bool _isSpeaking = false;
@override
Stream<TtsEvent> get onEvent => _eventStreamController.stream;
/// 音频数据流
Stream<Uint8List> get onAudioData => _audioDataStreamController.stream;
/// 构造函数
VolcanoTtsService() {
_setupEventListeners();
_setupAudioDataListeners();
}
/// 设置事件监听
void _setupEventListeners() {
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(
(dynamic event) {
if (event is! Map) return;
final Map<dynamic, dynamic> eventMap = event;
final String? type = eventMap['type'] as String?;
switch (type) {
case 'started':
_isSpeaking = true;
_eventStreamController.add(TtsEvent(type: TtsEventType.started));
break;
case 'completed':
_isSpeaking = false;
_eventStreamController.add(TtsEvent(type: TtsEventType.completed));
break;
case 'canceled':
_isSpeaking = false;
_eventStreamController.add(TtsEvent(type: TtsEventType.canceled));
break;
case 'error':
_isSpeaking = false;
final errorCode = eventMap['errorCode'] as int? ?? 0;
final errorMessage = eventMap['errorMessage'] as String? ?? '未知错误';
_eventStreamController.add(TtsEvent(
type: TtsEventType.error,
error: errorMessage,
));
break;
}
},
onError: (Object error) {
debugPrint('TTS事件流错误: $error');
_eventStreamController.add(TtsEvent(
type: TtsEventType.error,
error: '事件流错误: $error',
));
},
);
}
/// 设置音频数据监听
void _setupAudioDataListeners() {
_audioDataSubscription = _audioDataChannel.receiveBroadcastStream().listen(
(dynamic data) {
if (data is Uint8List) {
_audioDataStreamController.add(data);
}
},
onError: (Object error) {
debugPrint('TTS音频数据流错误: $error');
},
);
}
@override
Future<bool> initialize({List<String>? supportedLanguages}) async {
if (_isInitialized) return true;
try {
// 从.env文件读取配置
final appId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? '';
final appToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
if (appId.isEmpty || appToken.isEmpty) {
debugPrint('火山语音初始化失败:缺少VOLCANO_SPEECH_APP_ID或VOLCANO_SPEECH_APP_TOKEN环境变量');
return false;
}
// 初始化火山语音TTS
final result = await _channel.invokeMethod<bool>('initialize', {
'appId': appId,
'appToken': appToken,
}) ?? false;
if (!result) {
debugPrint('火山语音TTS初始化失败');
return false;
}
_isInitialized = true;
return true;
} catch (e) {
debugPrint('火山语音TTS初始化异常:$e');
return false;
}
}
/// 检查初始化状态
Future<bool> _ensureInitialized() async {
if (!_isInitialized) {
return await initialize();
}
return true;
}
@override
Future<bool> setVoice(String voiceName) async {
if (!await _ensureInitialized()) return false;
try {
final result = await _channel.invokeMethod<bool>('setVoice', {
'voice': voiceName,
}) ?? false;
if (result) {
_currentVoice = voiceName;
}
return result;
} catch (e) {
debugPrint('设置语音失败:$e');
return false;
}
}
/// 设置是否启用音频数据回调
Future<bool> setEnableAudioCallback(bool enable) async {
if (!await _ensureInitialized()) return false;
try {
final result = await _channel.invokeMethod<bool>('enableAudioCallback', {
'enable': enable,
}) ?? false;
if (result) {
_isAudioCallbackEnabled = enable;
}
return result;
} catch (e) {
debugPrint('设置音频回调失败:$e');
return false;
}
}
@override
Future<bool> speakOnce(String text) async {
if (!await _ensureInitialized()) return false;
try {
// 单次播放
final result = await _channel.invokeMethod<bool>('speakOnce', {
'text': text,
}) ?? false;
return result;
} catch (e) {
debugPrint('单次播放失败:$e');
_eventStreamController.add(TtsEvent.error('单次播放失败:$e'));
return false;
}
}
@override
Future<bool> speakStream(String text) async {
if (!await _ensureInitialized()) return false;
try {
// 流式添加文本
final result = await _channel.invokeMethod<bool>('speakStream', {
'text': text,
}) ?? false;
return result;
} catch (e) {
debugPrint('流式播放失败:$e');
_eventStreamController.add(TtsEvent.error('流式播放失败:$e'));
return false;
}
}
@override
Future<bool> flushStream() async {
if (!await _ensureInitialized()) return false;
try {
// 刷新流式文本
return await _channel.invokeMethod<bool>('flushStream') ?? false;
} catch (e) {
debugPrint('刷新流式文本失败:$e');
return false;
}
}
@override
Future<bool> stop() async {
if (!_isInitialized) return false;
try {
return await _channel.invokeMethod<bool>('stop') ?? false;
} catch (e) {
debugPrint('停止播放失败:$e');
return false;
}
}
@override
Future<void> dispose() async {
if (_isSpeaking) {
await stop();
}
await _eventSubscription?.cancel();
await _audioDataSubscription?.cancel();
if (!_eventStreamController.isClosed) {
await _eventStreamController.close();
}
if (!_audioDataStreamController.isClosed) {
await _audioDataStreamController.close();
}
if (_isInitialized) {
await _channel.invokeMethod<bool>('release');
_isInitialized = false;
}
}
}

209
lib/data/services/spotify_service.dart

@ -0,0 +1,209 @@
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:deep_voice/data/models/appconfig.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:spotify_sdk/spotify_sdk.dart';
import 'package:get/get.dart';
import 'ai_service.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件
class SpotifService {
bool isInitialized = false;
// final GetStorage _storage = GetStorage();
late String SPOTIFY_CLIENT_ID;
late String SPOTIFY_REDIRECT_URL;
/// 初始化OpenAI服务
Future<bool> initialize() async {
if (!isInitialized) {
// final _env = _storage.read("ENV") as Map<String, String>;
SPOTIFY_CLIENT_ID = AppConfig.env('SPOTIFY_CLIENT_ID') ?? '';
SPOTIFY_REDIRECT_URL = AppConfig.env('SPOTIFY_REDIRECT_URL') ?? '';
printInfo(
info:
"SpotifService----------- initialize SPOTIFY_CLIENT_ID:${SPOTIFY_CLIENT_ID} SPOTIFY_REDIRECT_URL:${SPOTIFY_REDIRECT_URL}");
await getAccessToken();
await connectToSpotifyRemote();
isInitialized = true;
}
return true;
}
/// 获取Spotify SDK的版本信息
Future<void> connectToSpotifyRemote() async {
try {
var result = await SpotifySdk.connectToSpotifyRemote(
clientId: SPOTIFY_CLIENT_ID,
redirectUrl: SPOTIFY_REDIRECT_URL,
scope:
'user-read-private user-read-email streaming app-remote-control user-modify-playback-state',
);
printInfo(
info: "SpotifService----------- connectToSpotifyRemote $result");
} on PlatformException catch (e) {
printError(info: "SpotifService------------ PlatformException$e");
} on MissingPluginException {
printError(info: "SpotifService------------ MissingPluginException");
}
}
/// 获取Spotify的访问令牌
Future<String> getAccessToken() async {
try {
var authenticationToken = await SpotifySdk.getAccessToken(
clientId: SPOTIFY_CLIENT_ID,
redirectUrl: SPOTIFY_REDIRECT_URL,
scope: 'app-remote-control, '
'user-modify-playback-state, '
'playlist-read-private, '
'playlist-modify-public,user-read-currently-playing',
);
printInfo(
info: "SpotifService----------- getAccessToken $authenticationToken");
return authenticationToken;
} on PlatformException catch (e) {
printError(
info:
"SpotifService------------getAccessToken ${e.code}: ${e.message}");
return Future.error('$e.code: $e.message');
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
return Future.error('not implemented');
}
}
//获取当前播放状态
Future<bool> isPaused() async {
try {
var state = await SpotifySdk.getPlayerState();
if (state != null) {
printInfo(info: "SpotifService----------- isPaused $state");
return state.isPaused;
} else {
printError(info: "SpotifService----------- isPaused null");
return true;
}
} on PlatformException catch (e) {
printError(
info:
"SpotifService------------getPlayerState ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getPlayerState not implemented");
}
return false;
}
//播放 音乐
Future<void> play(String url) async {
try {
await SpotifySdk.play(spotifyUri: url);
printInfo(info: "SpotifService----------- play $url");
} on PlatformException catch (e) {
printError(info: "SpotifService------------play ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//暂停 音乐
Future<void> pause() async {
try {
await SpotifySdk.pause();
printInfo(info: "SpotifService----------- pause");
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//停止 音乐
Future<void> resume() async {
try {
await SpotifySdk.resume();
printInfo(info: "SpotifService----------- resume");
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//下一首 音乐
Future<void> skipNext() async {
try {
await SpotifySdk.skipNext();
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//跳过上一首
Future<void> skipPrevious() async {
try {
await SpotifySdk.skipPrevious();
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//获取当前播放状态
Future<void> seekTo() async {
try {
await SpotifySdk.seekTo(positionedMilliseconds: 20000);
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//获取当前播放状态
Future<void> seekToRelative() async {
try {
await SpotifySdk.seekToRelativePosition(relativeMilliseconds: 20000);
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//获取当前播放状态
Future<void> switchToLocalDevice() async {
try {
await SpotifySdk.switchToLocalDevice();
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
}

15
lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart

@ -267,12 +267,12 @@ class AndroidVoiceInteractionService extends GetxService {
}
/// 保存聊天记录
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) {
Future<void> _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) async {
try {
// 获取ChatHistoryService实例
final chatHistoryService = Get.find<ChatHistoryService>();
if (userMessage.isEmpty) {
return;
}
// 创建用户消息和助手消息
final userMsg = Message(
role: 'user',
content: userMessage,
@ -285,14 +285,17 @@ class AndroidVoiceInteractionService extends GetxService {
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息
);
// 获取ChatHistoryService实例
final chatHistoryService = Get.find<ChatHistoryService>();
// 加载现有历史记录
final existingMessages = chatHistoryService.loadHistory(agentId);
final existingMessages = await chatHistoryService.loadHistory(agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
chatHistoryService.saveHistory(agentId, existingMessages);
await chatHistoryService.saveHistory(agentId, existingMessages);
} catch (e) {
Logger.error('保存聊天记录失败: $e');
}

8
lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart

@ -250,7 +250,7 @@ class IosVoiceInteractionService extends GetxService {
);
// 保存聊天历史
_saveChatHistory(userMsg, assistantMsg);
await _saveChatHistory(userMsg, assistantMsg);
// 发送聊天历史事件
_sendChatHistoryEvent(userMsg.content, assistantMsg.content);
@ -424,16 +424,16 @@ class IosVoiceInteractionService extends GetxService {
}
// 保存聊天历史
void _saveChatHistory(Message userMsg, Message assistantMsg) {
Future<void> _saveChatHistory(Message userMsg, Message assistantMsg) async {
try {
// 加载现有历史记录
final existingMessages = _chatHistoryService.loadHistory(_agentId);
final existingMessages = await _chatHistoryService.loadHistory(_agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
_chatHistoryService.saveHistory(_agentId, existingMessages);
await _chatHistoryService.saveHistory(_agentId, existingMessages);
Logger.info('保存聊天历史记录成功');
} catch (e) {

275
lib/data/services/voice_interaction_service.dart

@ -1,275 +0,0 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/events/voice_interaction_event.dart';
import '../../core/utils/logger.dart';
import '../../modules/chat/models/message_model.dart';
import 'chat_history_service.dart';
/// 语音交互服务接口
///
/// 管理与平台原生语音交互服务的通信,提供统一的接口供应用使用
class VoiceInteractionService extends GetxService {
static VoiceInteractionService get to => Get.find<VoiceInteractionService>();
// 方法通道
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction');
// 事件通道
static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events');
// 服务状态
final _isServiceRunning = false.obs;
bool get isServiceRunning => _isServiceRunning.value;
// 流控制器
final _eventStreamController = StreamController<VoiceInteractionEvent>.broadcast();
// 事件流
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream;
// 事件通道订阅
StreamSubscription? _eventSubscription;
// 标记是否初始化
bool _isInitialized = false;
// 配置信息
late String _azureSpeechKey;
late String _azureSpeechRegion;
late String _volcanoAiKey;
// 聊天历史服务
late final ChatHistoryService _chatHistoryService;
/// 设置事件通道
void _setupEventChannel() {
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen(_handleVoiceInteractionEvent, onError: (error) {
Logger.error('语音交互事件通道错误: $error');
});
}
/// 从环境变量加载配置
void _loadConfig() {
_azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
_volcanoAiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? '';
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) {
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
if (_volcanoAiKey.isEmpty) {
Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY');
}
}
/// 处理来自原生层的事件
void _handleVoiceInteractionEvent(dynamic event) {
if (event is! Map) return;
final eventMap = event as Map<dynamic, dynamic>;
final String eventType = eventMap['type'] as String? ?? '';
// final int timestamp = eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch;
Logger.info('收到语音交互事件: $eventType');
switch (eventType) {
case 'recognition_started':
// 语音识别开始事件
final recognitionEvent = RecognitionStartedEvent(
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(recognitionEvent);
break;
case 'chat_history_updated':
// 聊天历史更新事件
final String agentId = eventMap['agentId'] as String? ?? '';
final String userMessage = eventMap['userMessage'] as String? ?? '';
final String assistantMessage = eventMap['assistantMessage'] as String? ?? '';
final chatHistoryEvent = ChatHistoryEvent(
agentId: agentId,
userMessage: userMessage,
assistantMessage: assistantMessage,
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(chatHistoryEvent);
// 保存聊天历史到ChatHistoryService
_saveChatHistory(agentId, userMessage, assistantMessage, DateTime.now().millisecondsSinceEpoch);
break;
}
}
/// 保存聊天历史
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) {
try {
// 检查参数有效性
if (userMessage.isEmpty) {
return;
}
// 创建用户消息和助手消息
final userMsg = Message(
role: 'user',
content: userMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp),
);
final assistantMsg = Message(
role: 'assistant',
content: assistantMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息
);
// 加载现有历史记录
final existingMessages = _chatHistoryService.loadHistory(agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
_chatHistoryService.saveHistory(agentId, existingMessages);
Logger.info('已保存聊天历史: agentId=$agentId');
} catch (e) {
Logger.error('保存聊天历史失败: $e');
}
}
/// 初始化服务
Future<bool> initialize() async {
if (_isInitialized) return true;
try {
Logger.info('正在初始化语音交互服务...');
// 获取聊天历史服务
try {
_chatHistoryService = Get.find<ChatHistoryService>();
} catch (e) {
Logger.warning('获取ChatHistoryService失败,将创建新实例');
_chatHistoryService = Get.put(ChatHistoryService());
}
// 设置事件通道
_setupEventChannel();
// 加载配置
_loadConfig();
// 检查服务是否已在运行
final bool running = await checkServiceStatus();
// 如果服务未运行,启动服务
if (!running) {
// 启动语音交互服务
final success = await startService();
if (!success) {
Logger.error('语音交互服务启动失败');
return false;
}
}
_isInitialized = true;
Logger.info('语音交互服务初始化完成');
return true;
} catch (e) {
Logger.error('语音交互服务初始化失败: $e');
return false;
}
}
/// 启动语音交互服务
Future<bool> startService() async {
try {
Logger.info('启动语音交互服务...');
// 使用已加载的配置信息
final result = await _channel.invokeMethod<bool>('startService', {
'azure_speech_key': _azureSpeechKey,
'azure_speech_region': _azureSpeechRegion,
'volcano_ai_api_key': _volcanoAiKey,
}) ?? false;
if (result) {
_isServiceRunning.value = true;
Logger.info('语音交互服务已启动');
} else {
Logger.error('启动语音交互服务失败');
}
return result;
} catch (e) {
Logger.error('启动语音交互服务时发生错误: $e');
return false;
}
}
/// 停止语音交互服务
Future<bool> stopService() async {
try {
Logger.info('停止语音交互服务...');
final result = await _channel.invokeMethod<bool>('stopService') ?? false;
if (result) {
_isServiceRunning.value = false;
Logger.info('语音交互服务已停止');
} else {
Logger.error('停止语音交互服务失败');
}
return result;
} catch (e) {
Logger.error('停止语音交互服务时发生错误: $e');
return false;
}
}
/// 检查服务是否运行
Future<bool> checkServiceStatus() async {
try {
final bool result = await _channel.invokeMethod<bool>('isServiceRunning') ?? false;
_isServiceRunning.value = result;
return result;
} catch (e) {
Logger.error('检查服务状态时发生错误: $e');
return false;
}
}
/// 暂停语音交互
Future<bool> pauseVoiceInteraction() async {
try {
Logger.info('暂停后台语音交互...');
final result = await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false;
if (result) {
Logger.info('语音交互已暂停');
} else {
Logger.error('暂停语音交互失败');
}
return result;
} catch (e) {
Logger.error('暂停语音交互时发生错误: $e');
return false;
}
}
/// 资源释放
@override
void onClose() {
_eventSubscription?.cancel();
_eventStreamController.close();
super.onClose();
}
}

6
lib/data/services/volcano_ai_service.dart

@ -35,7 +35,8 @@ class VolcanoAIService implements AiService {
@override
Future<String> sendMessage({
required List<Map<String, String>> messages,
required String systemPrompt,
String? systemPrompt,
String? userProperties,
}) async {
final url = Uri.parse('$_baseUrl$_chatEndpoint');
@ -84,7 +85,8 @@ class VolcanoAIService implements AiService {
@override
Stream<String> sendMessageStream({
required List<Map<String, String>> messages,
required String systemPrompt,
String? systemPrompt,
String? userProperties,
}) async* {
final url = Uri.parse('$_baseUrl$_chatEndpoint');

137
lib/data/services/volcano_translation_service.dart

@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:math';
import 'package:deep_voice/data/models/appconfig.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
@ -10,7 +12,7 @@ import 'language_manager.dart';
class VolcanoTranslationService extends GetxService {
// API 配置
late String _baseUrl;
final String _endpoint = '/'; // 修改为根路径
final String _endpoint = '/'; // 修改为根路径
late final String _accessKey;
late final String _secretKey;
late final String _region;
@ -21,7 +23,7 @@ class VolcanoTranslationService extends GetxService {
// 语言管理器
final LanguageManager _languageManager = Get.find<LanguageManager>();
// final GetStorage _storage = GetStorage();
// 构造函数
VolcanoTranslationService();
@ -32,9 +34,13 @@ class VolcanoTranslationService extends GetxService {
String? region,
}) async {
try {
_accessKey = accessKey ?? dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? '';
_secretKey = secretKey ?? dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? '';
_region = region ?? dotenv.env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1';
// final _env = _storage.read("ENV") as Map<String, String>;
_accessKey =
accessKey ?? AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') ?? '';
_secretKey =
secretKey ?? AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') ?? '';
_region =
region ?? AppConfig.env('VOLCANO_TRANSLATION_REGION') ?? 'cn-north-1';
_service = 'translate';
_baseUrl = 'https://translate.volcengineapi.com';
@ -50,14 +56,21 @@ class VolcanoTranslationService extends GetxService {
}
// 生成签名
Map<String, String> _generateSignature(String method, Map<String, dynamic> requestBody, Map<String, String> queryParams) {
Map<String, String> _generateSignature(String method,
Map<String, dynamic> requestBody, Map<String, String> queryParams) {
// 1. 准备时间相关参数
final now = DateTime.now().toUtc();
final date = now.toIso8601String().split('T')[0].replaceAll('-', '');
final timestamp = now.toIso8601String().replaceAll(':', '').replaceAll('-', '').split('.')[0] + 'Z';
final timestamp = now
.toIso8601String()
.replaceAll(':', '')
.replaceAll('-', '')
.split('.')[0] +
'Z';
// 2. 构建规范查询字符串 - 按照参数名ASCII排序
final sortedParams = Map.fromEntries(queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key)));
final sortedParams = Map.fromEntries(
queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key)));
final List<String> queryParts = [];
sortedParams.forEach((key, value) {
// 使用RFC 3986编码,确保特殊字符被正确处理
@ -66,48 +79,53 @@ class VolcanoTranslationService extends GetxService {
queryParts.add('$encodedKey=$encodedValue');
});
final canonicalQueryString = queryParts.join('&');
// 3. 创建规范请求
final contentType = 'application/json';
final payloadHash = sha256.convert(utf8.encode(json.encode(requestBody))).toString();
final payloadHash =
sha256.convert(utf8.encode(json.encode(requestBody))).toString();
final host = 'translate.volcengineapi.com';
// 确保头部按照字典序排序,且键名小写
// 注意:只包含host和x-date头部,与示例保持一致
final canonicalHeaders = 'host:$host\nx-date:$timestamp\n';
final signedHeaders = 'host;x-date';
// 构建规范请求,格式必须与示例完全一致
final canonicalRequest = '$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash';
final canonicalRequest =
'$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash';
// 4. 创建待签字符串
final algorithm = 'HMAC-SHA256';
final credentialScope = '$date/$_region/$_service/request';
final stringToSign = '$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}';
final stringToSign =
'$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}';
// 5. 计算签名 - 按照文档正确派生密钥
// 注意:这里是关键修复点,确保密钥派生过程完全符合文档
final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀
final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀
// kDate = HMAC(kSecret, Date)
var kDate = Hmac(sha256, kSecret).convert(utf8.encode(date)).bytes;
// kRegion = HMAC(kDate, Region)
var kRegion = Hmac(sha256, kDate).convert(utf8.encode(_region)).bytes;
// kService = HMAC(kRegion, Service)
var kService = Hmac(sha256, kRegion).convert(utf8.encode(_service)).bytes;
// kSigning = HMAC(kService, "request")
var kSigning = Hmac(sha256, kService).convert(utf8.encode('request')).bytes;
// 使用派生的签名密钥计算最终签名
final signature = Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString();
final signature =
Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString();
// 6. 添加签名到请求头 - 格式必须与示例完全一致
final authorization = '$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature';
final authorization =
'$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature';
// 只返回必要的头部,与示例保持一致
return {
'Content-Type': contentType,
@ -131,27 +149,29 @@ class VolcanoTranslationService extends GetxService {
// 获取源语言和目标语言的短代码
String sourceCode = '';
String targetCode = 'zh';
// 从 ASR 代码获取短代码
if (sourceLanguageCode.isNotEmpty) {
final sourceShortCode = _languageManager.getShortCodeByAsrCode(sourceLanguageCode);
final sourceShortCode =
_languageManager.getShortCodeByAsrCode(sourceLanguageCode);
if (sourceShortCode != null) {
sourceCode = sourceShortCode;
}
}
final targetShortCode = _languageManager.getShortCodeByAsrCode(targetLanguageCode);
final targetShortCode =
_languageManager.getShortCodeByAsrCode(targetLanguageCode);
if (targetShortCode != null) {
targetCode = targetShortCode;
}
// 构建请求体
final requestBody = {
'SourceLanguage': sourceCode,
'TargetLanguage': targetCode,
'TextList': [text],
};
// 添加必要的查询参数 - 确保包含所有必需参数
final queryParams = {
'Action': 'TranslateText',
@ -159,60 +179,61 @@ class VolcanoTranslationService extends GetxService {
'Region': _region,
'Service': _service,
};
// 生成包含查询参数的签名
final headers = _generateSignature('POST', requestBody, queryParams);
// 构建带有查询参数的URL
final queryString = Uri(queryParameters: queryParams).query;
final url = '$_baseUrl/?$queryString';
final response = await http.post(
Uri.parse(url),
headers: headers,
body: json.encode(requestBody),
);
if (response.statusCode == 200) {
final responseData = json.decode(response.body);
// 尝试从不同的响应结构中提取翻译结果
String? translation;
// 1. 标准响应结构
if (responseData['TranslationList'] != null &&
responseData['TranslationList'] is List &&
if (responseData['TranslationList'] != null &&
responseData['TranslationList'] is List &&
responseData['TranslationList'].isNotEmpty) {
final translationItem = responseData['TranslationList'][0];
if (translationItem is Map && translationItem.containsKey('Translation')) {
if (translationItem is Map &&
translationItem.containsKey('Translation')) {
translation = translationItem['Translation'];
}
}
// 2. 检查是否有错误响应
if (translation == null &&
responseData['ResponseMetadata'] != null &&
responseData['ResponseMetadata'] is Map &&
if (translation == null &&
responseData['ResponseMetadata'] != null &&
responseData['ResponseMetadata'] is Map &&
responseData['ResponseMetadata']['Error'] != null) {
return null;
}
// 3. 尝试其他可能的响应结构
if (translation == null) {
if (responseData.containsKey('Translation')) {
translation = responseData['Translation'];
} else if (responseData.containsKey('Result') &&
responseData['Result'] is Map &&
responseData['Result'].containsKey('Translation')) {
} else if (responseData.containsKey('Result') &&
responseData['Result'] is Map &&
responseData['Result'].containsKey('Translation')) {
translation = responseData['Result']['Translation'];
} else if (responseData.containsKey('Data') &&
responseData['Data'] is Map) {
} else if (responseData.containsKey('Data') &&
responseData['Data'] is Map) {
final data = responseData['Data'];
if (data.containsKey('Translation')) {
translation = data['Translation'];
} else if (data.containsKey('TranslationList') &&
data['TranslationList'] is List &&
data['TranslationList'].isNotEmpty) {
} else if (data.containsKey('TranslationList') &&
data['TranslationList'] is List &&
data['TranslationList'].isNotEmpty) {
final item = data['TranslationList'][0];
if (item is Map && item.containsKey('Translation')) {
translation = item['Translation'];
@ -220,14 +241,14 @@ class VolcanoTranslationService extends GetxService {
}
}
}
// 返回提取到的翻译结果
return translation;
}
return null;
} catch (e) {
return null;
}
}
}
}

9
lib/modules/agent/bindings/agent_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
class AgentBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<AgentController>(() => AgentController());
}
}

572
lib/modules/agent/controllers/agent_controller.dart

@ -0,0 +1,572 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart';
import 'package:logger/logger.dart';
import 'package:chat_storage/chat_storage.dart';
import 'package:image_picker/image_picker.dart';
class Message {
final bool isUser;
final String text;
final DateTime timestamp;
final bool isRecognizing; // 是否为语音识别中的临时消息
final bool hasImage; // 是否包含图片
final String? imagePath; // 图片路径
Message({
required this.isUser,
required this.text,
DateTime? timestamp,
this.isRecognizing = false,
this.hasImage = false,
this.imagePath,
}) : this.timestamp = timestamp ?? DateTime.now();
}
class AgentController extends GetxController {
static AgentController get to => Get.find();
final logger = Logger();
// 聊天消息列表
final messages = <Message>[].obs;
// 控制器
final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController();
// 状态标志
final isInitialized = false.obs;
final isListening = false.obs;
final isSpeaking = false.obs;
final isProcessing = false.obs;
// 图片处理状态
final isImageProcessing = false.obs;
// 输入模式控制
final isTextInputMode = true.obs;
// 当前输入的文本内容
final currentText = ''.obs;
// 图片输入相关
final isImageInputActive = false.obs; // 是否处于图片输入模式
final selectedImagePath = Rx<String?>(null); // 当前选择的图片路径
// 流事件订阅
StreamSubscription<AgentServiceEvent>? _eventSubscription;
// 用于跟踪AI回复状态
bool _isNewAssistantResponse = true;
String _currentResponseId = '';
// 会话ID
final String sessionId = 'default_agent';
// 是否已加载聊天记录
final isHistoryLoaded = false.obs;
@override
void onInit() {
super.onInit();
_loadChatHistory();
_subscribeToEvents();
}
@override
void onClose() {
AgentService.stopConversation();
AgentService.stopTts();
textController.dispose();
scrollController.dispose();
_eventSubscription?.cancel();
super.onClose();
}
// 加载聊天历史记录
Future<void> _loadChatHistory() async {
try {
logger.i('正在加载聊天历史...');
final result = await ChatStorage.getMessages(
sessionId: sessionId,
page: 1,
pageSize: 20,
);
if (result.messages.isNotEmpty) {
// 转换并添加消息到列表
final historyMessages = result.messages.map((msg) => Message(
isUser: msg.sender == 'user',
text: msg.message,
timestamp: DateTime.parse(msg.timestamp),
)).toList();
// 消息按时间排序,最新的消息在底部
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messages.assignAll(historyMessages);
logger.i('成功加载 ${historyMessages.length} 条聊天记录');
} else {
// 没有历史记录时添加欢迎消息
_addWelcomeMessage();
logger.i('没有历史记录,添加欢迎消息');
}
} catch (e) {
logger.e('加载聊天历史失败: $e');
_addWelcomeMessage();
} finally {
isHistoryLoaded.value = true;
messages.refresh();
// 在加载完成后直接定位到底部,无需动画
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
}
}
// 添加欢迎消息
void _addWelcomeMessage() {
messages.add(Message(
isUser: false,
text: '你好!我是DeepVoice AI助手,我能帮你:\n\n'
'• 回答问题和提供信息\n'
'• 实时语音翻译\n'
'• 推荐音乐和播客\n'
'• 语音备忘录和提醒\n'
'• 控制耳机功能\n\n'
'有什么我可以帮你的吗?',
));
}
// 订阅代理服务事件
void _subscribeToEvents() {
_eventSubscription = AgentService.events.listen((event) {
switch (event.type) {
case AgentServiceEventType.recognitionStarted:
isListening.value = true;
isTextInputMode.value = false;
isTextInputMode.refresh();
// 只标记识别开始,不添加消息
break;
case AgentServiceEventType.recognitionStopped:
isListening.value = false;
isTextInputMode.value = true;
isTextInputMode.refresh();
break;
case AgentServiceEventType.recognizing:
final text = event.data['text'] ?? '';
currentText.value = text; // 保留当前文本,以便其他地方使用
if (text.isNotEmpty) {
// 查找是否有正在识别中的消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
// 更新已有的识别中消息
messages[index] = Message(
isUser: true,
text: text,
isRecognizing: true,
timestamp: messages[index].timestamp,
);
} else {
// 没有识别中的消息,创建新消息
messages.add(Message(
isUser: true,
text: text,
isRecognizing: true,
));
}
messages.refresh();
_scrollToBottom();
}
break;
case AgentServiceEventType.recognitionResult:
final text = event.data['text'] ?? '';
// 查找是否有正在识别中的消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (text.isNotEmpty) {
if (index >= 0) {
// 将识别中的消息转换为最终消息
messages[index] = Message(
isUser: true,
text: text,
timestamp: messages[index].timestamp,
);
messages.refresh();
} else {
// 如果没有找到识别中的消息,添加新消息
messages.add(Message(isUser: true, text: text));
}
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 滚动到底部
_scrollToBottom();
} else if (index >= 0) {
// 如果识别结果为空,移除临时的识别消息
messages.removeAt(index);
messages.refresh();
}
break;
case AgentServiceEventType.ttsStarted:
isSpeaking.value = true;
break;
case AgentServiceEventType.ttsCompleted:
case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled:
isSpeaking.value = false;
break;
case AgentServiceEventType.imageProcessing:
isImageProcessing.value = true;
break;
case AgentServiceEventType.imageReady:
// 图片准备就绪,但AI还没有开始处理,保持processing状态
break;
case AgentServiceEventType.assistantToken:
if (!isProcessing.value) isProcessing.value = true;
final token = event.data['token'] ?? '';
final responseId = event.data['responseId'] ?? '';
if (token.isNotEmpty) {
// 如果是新的回复或者响应ID改变,创建新消息
if (_isNewAssistantResponse || (_currentResponseId != responseId && responseId.isNotEmpty)) {
messages.add(Message(isUser: false, text: token));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 否则更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text + token,
timestamp: messages[lastIndex].timestamp,
);
}
}
// 滚动到底部
_scrollToBottom();
}
break;
case AgentServiceEventType.assistantResponse:
isProcessing.value = false;
isImageProcessing.value = false;
// 标记当前回复完成
_isNewAssistantResponse = true;
break;
case AgentServiceEventType.error:
isListening.value = false;
isSpeaking.value = false;
isProcessing.value = false;
isImageProcessing.value = false;
// 移除临时的识别消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
messages.removeAt(index);
messages.refresh();
}
logger.e('代理服务错误: ${event.data['message']}');
break;
case AgentServiceEventType.autoStop:
isListening.value = false;
// 移除临时的识别消息
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
// 只有当消息为空或文本太短时才移除
if (messages[index].text.trim().length < 2) {
messages.removeAt(index);
messages.refresh();
} else {
// 否则转换为正式消息
messages[index] = Message(
isUser: true,
text: messages[index].text,
timestamp: messages[index].timestamp,
);
messages.refresh();
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text, speakResponse: true);
}
}
break;
case AgentServiceEventType.functionCall:
// final functionName = event.data['name'] ?? '';
// final arguments = event.data['arguments'] ?? '';
// logger.i('mcp: $functionName, $arguments');
break;
case AgentServiceEventType.functionCallResult:
final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? '';
break;
default:
break;
}
});
}
// 滚动到底部
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} catch (e) {
logger.e('滚动到底部失败: $e');
}
}
});
}
// 发送文本消息
Future<void> sendTextMessage() async {
// 如果图片输入是活跃的,使用选中的图片和文本框的内容
if (isImageInputActive.value && selectedImagePath.value != null) {
final text = textController.text.trim();
final imagePath = selectedImagePath.value!; // 使用!强制断言非空
await sendImageMessage(imagePath, text: text);
// 清除图片输入状态
clearImageInput();
return;
}
final text = textController.text.trim();
if (text.isEmpty) return;
final message = Message(isUser: true, text: text);
messages.add(message);
textController.clear();
// 滚动到底部
_scrollToBottom();
try {
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
await AgentService.processTextInput(text, speakResponse: true);
} catch (e) {
logger.e('发送文本消息失败: $e');
isProcessing.value = false;
}
}
// 发送图片消息
Future<void> sendImageMessage(String imagePath, {String? text}) async {
if (imagePath.isEmpty || !File(imagePath).existsSync()) {
logger.e('图片不存在: $imagePath');
return;
}
// 显示的文本,如果没有提供则使用默认值
final displayText = text?.isNotEmpty == true ? text! : '[图片]';
// 添加用户消息
final message = Message(
isUser: true,
text: displayText,
hasImage: true,
imagePath: imagePath,
);
messages.add(message);
// 清空文本输入
textController.clear();
// 滚动到底部
_scrollToBottom();
try {
isProcessing.value = true;
isImageProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 调用Agent Service处理图片
await AgentService.processImageInput(
imagePath,
text: text ?? '',
speakResponse: true,
);
} catch (e) {
logger.e('处理图片失败: $e');
isProcessing.value = false;
isImageProcessing.value = false;
}
}
// 开始语音输入
Future<void> startVoiceInput() async {
if (isListening.value) return;
try {
await AgentService.startConversation();
} catch (e) {
logger.e('启动语音输入失败: $e');
}
}
// 停止语音输入
Future<void> stopVoiceInput() async {
if (!isListening.value) return;
try {
await AgentService.stopConversation();
} catch (e) {
logger.e('停止语音输入失败: $e');
}
}
// 清除聊天历史
Future<void> clearChat() async {
try {
await AgentService.clearChatHistory();
// 清空本地消息列表
messages.clear();
// 添加欢迎消息
_addWelcomeMessage();
// 重置回复状态
_isNewAssistantResponse = true;
} catch (e) {
logger.e('清除聊天历史失败: $e');
}
}
// 切换输入模式
void toggleInputMode() {
// 如果当前在图片输入模式,先清除
if (isImageInputActive.value) {
clearImageInput();
return;
}
isTextInputMode.toggle();
// 切换到语音模式时,直接开始语音输入
if (!isTextInputMode.value) {
startVoiceInput();
}
// 切换到文本模式时,直接停止语音输入
else {
stopVoiceInput();
}
}
// 从相册选择图片
Future<void> pickImage() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80, // 设置图片质量
maxWidth: 1024, // 限制最大宽度
maxHeight: 1024, // 限制最大高度
);
if (image != null) {
// 设置已选择的图片
selectedImagePath.value = image.path;
// 激活图片输入模式
isImageInputActive.value = true;
}
} catch (e) {
logger.e('选择图片失败: $e');
}
}
// 拍照获取图片
Future<void> takePhoto() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? photo = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 80, // 设置图片质量
maxWidth: 1024, // 限制最大宽度
maxHeight: 1024, // 限制最大高度
);
if (photo != null) {
// 设置已选择的图片
selectedImagePath.value = photo.path;
// 激活图片输入模式
isImageInputActive.value = true;
}
} catch (e) {
logger.e('拍照失败: $e');
}
}
// 清除图片输入状态
void clearImageInput() {
selectedImagePath.value = null;
isImageInputActive.value = false;
}
// 使用预设提示词处理图片
void useImagePrompt(String prompt) {
// 确保selectedImagePath.value不为null
if (selectedImagePath.value == null) return;
// 填充文本框同时直接发送消息
textController.text = prompt;
// 立即发送图片和提示词
final imagePath = selectedImagePath.value!;
sendImageMessage(imagePath, text: prompt);
// 发送后清除图片输入状态
clearImageInput();
}
// 预设提示词:总结图片内容
void summarizeImage() {
useImagePrompt('请总结这张图片的内容');
}
// 预设提示词:抽取图像文字
void extractTextFromImage() {
useImagePrompt('请提取这张图片中的所有文字');
}
// 预设提示词:翻译图像文字
void translateImageText() {
useImagePrompt('请翻译这张图片中的文字');
}
}

502
lib/modules/agent/views/agent_view.dart

@ -0,0 +1,502 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
import 'message_bubble.dart';
class AgentView extends StatelessWidget {
const AgentView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final controller = Get.find<AgentController>();
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.smart_toy_outlined,
color: Colors.white,
),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'DeepVoice AI',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(width: 4),
Text(
'在线',
style: TextStyle(
fontSize: 12,
color: Colors.green,
),
),
],
),
],
),
],
),
actions: [
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
child: const Text('清除聊天记录'),
onTap: () => controller.clearChat(),
),
],
),
],
),
body: Column(
children: [
// 聊天内容
Expanded(
child: Obx(
() => ListView.builder(
controller: controller.scrollController,
reverse: false,
padding: const EdgeInsets.all(16),
itemCount: controller.messages.length,
itemBuilder: (context, index) {
final message = controller.messages[index];
return MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
);
},
),
),
),
// 输入区域
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, -1),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 图片预览区域 - 仅在图片输入模式下显示
Obx(() {
if (controller.isImageInputActive.value && controller.selectedImagePath.value != null) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 图片预览和关闭按钮
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
File(controller.selectedImagePath.value!),
height: 150,
width: double.infinity,
fit: BoxFit.cover,
),
),
Positioned(
top: 8,
right: 8,
child: GestureDetector(
onTap: () => controller.clearImageInput(),
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
shape: BoxShape.circle,
),
child: Icon(
Icons.close,
color: Colors.white,
size: 16,
),
),
),
),
],
),
// 预设提示选项
SizedBox(height: 8),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildPromptChip(
context: context,
label: '总结图片内容',
onTap: () => controller.summarizeImage(),
),
SizedBox(width: 8),
_buildPromptChip(
context: context,
label: '抽取图像文字',
onTap: () => controller.extractTextFromImage(),
),
SizedBox(width: 8),
_buildPromptChip(
context: context,
label: '翻译图像文字',
onTap: () => controller.translateImageText(),
),
],
),
),
SizedBox(height: 12),
],
);
} else {
return SizedBox.shrink();
}
}),
// 输入控件行
Row(
children: [
// 键盘/语音切换按钮
Obx(() {
final isTextMode = controller.isTextInputMode.value;
return GestureDetector(
onTap: () {
controller.toggleInputMode();
},
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
),
child: Icon(
isTextMode ? Icons.mic_none : Icons.keyboard,
color: Colors.grey[600],
),
),
);
}),
const SizedBox(width: 8),
// 文本输入框或语音波形图
Expanded(
child: Obx(() {
final isTextMode = controller.isTextInputMode.value;
if (isTextMode) {
// 文本输入模式 - 注意观察图片模式时的高度变化
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
),
child: Row(
children: [
Expanded(
child: Obx(() {
// 图片模式下使用更高的输入框
final isImageActive = controller.isImageInputActive.value;
return TextField(
controller: controller.textController,
maxLines: isImageActive ? 2 : 1,
minLines: isImageActive ? 2 : 1,
decoration: InputDecoration(
hintText: isImageActive ? '添加对图片的问题或描述...' : '输入消息...',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: isImageActive ? 12 : 0),
),
onSubmitted: (_) => controller.sendTextMessage(),
);
}),
),
GestureDetector(
onTap: () => controller.sendTextMessage(),
child: Icon(
Icons.send,
color: Theme.of(context).primaryColor,
),
),
],
),
);
} else {
// 语音输入模式 - 始终显示波形图
return GestureDetector(
onTap: () {
if (controller.isListening.value) {
controller.stopVoiceInput();
} else {
controller.startVoiceInput();
}
},
child: Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
),
child: Row(
children: [
// 语音波形图 - 始终显示
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
...List.generate(10, (index) {
return _buildSoundBar(index);
}),
const Spacer(),
// "正在聆听"文字
Obx(() {
final isListening = controller.isListening.value;
return Row(
children: [
Icon(
Icons.mic,
size: 14,
color: isListening
? Theme.of(context).primaryColor
: Colors.grey,
),
const SizedBox(width: 4),
Text(
'正在聆听',
style: TextStyle(
fontSize: 12,
color: isListening
? Theme.of(context).primaryColor
: Colors.grey,
),
),
],
);
}),
],
),
),
],
),
),
);
}
}),
),
const SizedBox(width: 8),
// 图片选择按钮
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
),
child: PopupMenuButton<String>(
icon: Icon(
Icons.add_photo_alternate_outlined,
color: Colors.grey[600],
),
padding: EdgeInsets.zero,
onSelected: (value) async {
if (value == 'camera') {
await controller.takePhoto();
} else if (value == 'gallery') {
await controller.pickImage();
}
},
itemBuilder: (context) => [
PopupMenuItem<String>(
value: 'camera',
child: Row(
children: [
Icon(Icons.camera_alt, color: Theme.of(context).primaryColor),
SizedBox(width: 8),
Text('拍照'),
],
),
),
PopupMenuItem<String>(
value: 'gallery',
child: Row(
children: [
Icon(Icons.photo_library, color: Theme.of(context).primaryColor),
SizedBox(width: 8),
Text('从相册选择'),
],
),
),
],
),
),
],
),
],
),
),
],
),
);
}
// 构建提示选项芯片
Widget _buildPromptChip({
required BuildContext context,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Theme.of(context).primaryColor.withOpacity(0.3),
),
),
child: Text(
label,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
);
}
// 构建声音柱
Widget _buildSoundBar(int index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
child: Obx(() => _SoundBar(
delay: index * 0.1,
isActive: Get.find<AgentController>().isListening.value,
)),
);
}
}
// 声音柱动画组件
class _SoundBar extends StatefulWidget {
final double delay;
final bool isActive;
const _SoundBar({
required this.delay,
required this.isActive,
});
@override
_SoundBarState createState() => _SoundBarState();
}
class _SoundBarState extends State<_SoundBar> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(begin: 3, end: 15).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
)..addListener(() {
setState(() {});
});
_startOrStopAnimation();
}
@override
void didUpdateWidget(_SoundBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isActive != oldWidget.isActive) {
_startOrStopAnimation();
}
}
void _startOrStopAnimation() {
if (widget.isActive) {
Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () {
if (mounted && widget.isActive) {
_controller.repeat(reverse: true);
}
});
} else {
_controller.reset();
// 设置为小高度的静态状态
_controller.animateTo(0.1, duration: Duration(milliseconds: 300));
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: 2,
height: widget.isActive ? _animation.value : 3,
decoration: BoxDecoration(
color: widget.isActive ? Theme.of(context).primaryColor : Colors.grey[400],
borderRadius: BorderRadius.circular(1),
),
);
}
}

879
lib/modules/agent/views/message_bubble.dart

@ -0,0 +1,879 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'dart:io';
/// 消息气泡组件
class MessageBubble extends StatelessWidget {
final bool isUser;
final String message;
final DateTime timestamp;
final bool isRecognizing;
final bool hasImage;
final String? imagePath;
const MessageBubble({
Key? key,
required this.isUser,
required this.message,
required this.timestamp,
this.isRecognizing = false,
this.hasImage = false,
this.imagePath,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final timeFormat = DateFormat('HH:mm');
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) _buildAvatar(isUser, context),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Container(
padding: hasImage ? (message == '[图片]' ? EdgeInsets.zero : const EdgeInsets.all(12)) : const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _getBubbleColor(context),
borderRadius: BorderRadius.circular(18).copyWith(
bottomLeft: isUser ? const Radius.circular(18) : const Radius.circular(4),
bottomRight: !isUser ? const Radius.circular(18) : const Radius.circular(4),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: _buildMessageContent(context),
),
const SizedBox(height: 4),
// 识别中的消息不显示时间
if (!isRecognizing)
Text(
timeFormat.format(timestamp),
style: TextStyle(
fontSize: 10,
color: Colors.grey[500],
fontWeight: FontWeight.w400,
),
),
],
),
),
const SizedBox(width: 8),
if (isUser) _buildAvatar(isUser, context),
],
),
);
}
/// 获取气泡颜色
Color _getBubbleColor(BuildContext context) {
if (isUser) {
return isRecognizing
? Color(0xFF3B82F6).withOpacity(0.8) // 识别中的用户消息使用半透明的蓝色
: Color(0xFF3B82F6); // 使用更明亮的蓝色
} else {
return Color(0xFFF1F5F9); // 更淡的灰色背景
}
}
/// 构建头像
Widget _buildAvatar(bool isUser, BuildContext context) {
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isUser ? Colors.grey[300] : Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Icon(
isUser ? Icons.person : Icons.smart_toy_outlined,
color: isUser ? Colors.grey[700] : Colors.white,
size: 20,
),
);
}
/// 构建消息内容
Widget _buildMessageContent(BuildContext context) {
// 如果是识别中的消息,添加特殊样式
if (isRecognizing) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
message,
style: const TextStyle(
color: Colors.white,
fontStyle: FontStyle.italic,
fontSize: 14,
height: 1.4,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
_buildTypingIndicator(),
],
);
}
// 如果是图片消息,显示图片
if (hasImage && imagePath != null) {
// 如果消息是默认的[图片]标记,只显示图片
if (message == '[图片]') {
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.file(
File(imagePath!),
width: 200,
height: 200,
fit: BoxFit.cover,
),
);
}
// 如果消息是自定义文本,显示文本+图片
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message,
style: TextStyle(
color: isUser ? Colors.white : Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
File(imagePath!),
width: 200,
height: 200,
fit: BoxFit.cover,
),
),
],
);
}
// 判断消息内容,显示特殊卡片
if (!isUser) {
if (message.contains('播放列表') || message.contains('音乐')) {
return _buildMusicContent(context);
} else if (message.contains('景点') || message.contains('旅游')) {
return _buildTravelContent(context);
} else if (message.contains('天气') || message.contains('气温') || message.contains('预报')) {
return _buildWeatherContent(context);
}
}
return Text(
message,
style: TextStyle(
color: isUser ? Colors.white : Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
);
}
/// 构建输入中的指示器
Widget _buildTypingIndicator() {
return SizedBox(
width: 40,
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildDot(100),
_buildDot(300),
_buildDot(500),
],
),
);
}
/// 构建动画点
Widget _buildDot(int milliseconds) {
return _AnimatedDot(delayMilliseconds: milliseconds);
}
/// 构建音乐内容卡片
Widget _buildMusicContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'以下是一些推荐的音乐:',
style: TextStyle(
color: Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.music_note,
size: 16,
color: Theme.of(context).primaryColor.withOpacity(0.7),
),
const SizedBox(width: 6),
Text(
'雨天时光 · 播放列表',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
],
),
const SizedBox(height: 10),
// 歌曲示例
_buildMusicItem('雨声与钢琴曲', '纯音乐', context),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem('Rainy Day', 'Coldplay', context),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem('雨天的爵士咖啡馆', '爵士合集', context),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'查看更多推荐',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.play_arrow, size: 16),
label: Text('播放全部'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.shuffle, size: 16),
label: Text('随机播放'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[700],
side: BorderSide(color: Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
],
),
],
);
}
/// 构建旅游内容卡片
Widget _buildTravelContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'以下是一些推荐的景点:',
style: TextStyle(
color: Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.place,
size: 16,
color: Color(0xFFE11D48),
),
const SizedBox(width: 6),
Text(
'热门景点 · 东京',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
],
),
const SizedBox(height: 10),
// 景点列表
_buildLocationItem('东京塔', '标志性建筑,可俯瞰东京全景', context),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildLocationItem('涩谷十字路口', '世界上最繁忙的十字路口之一', context),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildLocationItem('浅草寺', '东京最古老的寺庙', context),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'查看更多景点',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.map, size: 16),
label: Text('查看地图'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.bookmark_border, size: 16),
label: Text('保存到收藏'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[700],
side: BorderSide(color: Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
],
),
],
);
}
/// 构建音乐项目
Widget _buildMusicItem(String title, String artist, BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.music_note,
size: 18,
color: Theme.of(context).primaryColor,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Text(
artist,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
],
),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.play_circle, color: Theme.of(context).primaryColor),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 24,
),
],
),
);
}
/// 构建地点项目
Widget _buildLocationItem(String name, String description, BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.place,
size: 18,
color: Color(0xFFE11D48),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Text(
description,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
],
),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.arrow_forward, color: Colors.grey[400]),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 20,
),
],
),
);
}
/// 构建天气内容卡片
Widget _buildWeatherContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'当前天气情况:',
style: TextStyle(
color: Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'北京',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
SizedBox(height: 4),
Text(
'晴朗',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
),
SizedBox(height: 12),
Row(
children: [
Text(
'25°',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最高: 29°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
Text(
'最低: 18°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
],
),
],
),
],
),
Icon(
Icons.wb_sunny,
size: 48,
color: Colors.white,
),
],
),
SizedBox(height: 16),
Container(
height: 1,
color: Colors.white.withOpacity(0.2),
),
SizedBox(height: 16),
Text(
'未来预报',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildForecastDay('周一', Icons.wb_sunny, '28°'),
_buildForecastDay('周二', Icons.cloud, '25°'),
_buildForecastDay('周三', Icons.wb_cloudy, '26°'),
_buildForecastDay('周四', Icons.grain, '24°'),
_buildForecastDay('周五', Icons.wb_sunny, '27°'),
],
),
SizedBox(height: 16),
Container(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.info_outline,
size: 16,
color: Colors.white,
),
SizedBox(width: 6),
Flexible(
child: Text(
'今日紫外线指数较高,外出请做好防晒',
style: TextStyle(
fontSize: 12,
color: Colors.white,
),
),
),
],
),
),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.location_on, size: 16),
label: Text('更多地区'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.calendar_today, size: 16),
label: Text('15天预报'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[700],
side: BorderSide(color: Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
],
),
],
);
}
/// 构建单日天气预报
Widget _buildForecastDay(String day, IconData icon, String temp) {
return Column(
children: [
Text(
day,
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
SizedBox(height: 4),
Icon(
icon,
size: 20,
color: Colors.white,
),
SizedBox(height: 4),
Text(
temp,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
],
);
}
}
/// 动画点组件
class _AnimatedDot extends StatefulWidget {
final int delayMilliseconds;
const _AnimatedDot({required this.delayMilliseconds});
@override
_AnimatedDotState createState() => _AnimatedDotState();
}
class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
_animation = Tween<double>(begin: 0, end: 6).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeOut),
reverseCurve: Interval(0.5, 1.0, curve: Curves.easeIn),
),
)..addListener(() {
setState(() {});
});
Future.delayed(Duration(milliseconds: widget.delayMilliseconds), () {
if (mounted) {
_controller.repeat();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(3),
boxShadow: [
BoxShadow(
color: Colors.white.withOpacity(0.5),
blurRadius: _animation.value,
spreadRadius: _animation.value / 2,
),
],
),
);
}
}

600
lib/modules/chat/controllers/chat_controller.dart

@ -1,35 +1,42 @@
import 'dart:async';
import 'dart:convert';
import 'package:amap_flutter/amap_flutter.dart';
import '../../../data/services/spotify_service.dart';
import 'package:get/get.dart';
import 'package:flutter/widgets.dart';
import 'package:get_storage/get_storage.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import 'package:just_audio/just_audio.dart';
import 'package:chat_storage/chat_storage.dart';
import 'package:path/path.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/deapsound_ai_service.dart';
import '../../../data/services/kouzi_ai_service.dart';
import '../../../data/services/speech_impl/voice_clone_tts_service.dart';
import '../models/message_model.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/tts_service.dart';
import '../../../data/providers/agent_provider.dart';
import '../../../data/models/agent.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/chat_history_service.dart';
import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/voice_interaction_service.dart';
import '../../../data/services/open_ai_service_adapter.dart';
class ChatController extends GetxController {
// 服务
late AiService _aiService;
late TtsService _ttsService;
final AsrService _asrService = Get.find<AsrService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
final VoiceInteractionService _voiceInteractionService =
Get.find<VoiceInteractionService>();
late SpotifService _spotifService;
// 添加ScrollController用于控制列表滚动
final ScrollController scrollController = ScrollController();
final _amapFlutter = AmapFlutter();
StreamSubscription? _locationStream;
String _latLng = '';
String _city = '深圳市';
// Agent 信息
late final Agent agent;
@ -45,6 +52,13 @@ class ChatController extends GetxController {
final RxBool isTtsEnabled = true.obs;
final RxBool isVoiceRecognitionActive = false.obs;
// 分页加载状态
final RxInt _currentPage = 1.obs;
final RxInt _totalPages = 1.obs;
final RxBool _isLoadingMore = false.obs;
final RxBool _hasMoreHistory = true.obs;
static const int _pageSize = 20; // 每页加载的消息数量
// 语音交互模式
bool _isVoiceInteractionMode = false;
@ -53,9 +67,6 @@ class ChatController extends GetxController {
final RxBool isListening = false.obs;
final RxBool isSpeechDetected = false.obs;
// 临时消息索引,用于更新临时消息
int _tempMessageIndex = -1;
// 防抖计时器
Timer? _scrollDebounceTimer;
@ -73,10 +84,21 @@ class ChatController extends GetxController {
// 记录最后一次活动时间
DateTime _lastActivityTime = DateTime.now();
// 音乐播放
final player = AudioPlayer();
final RxString audiourl = ''.obs;
final RxBool isPlay = false.obs;
// 按住说话、文字输入切换
final RxBool isTextInput = false.obs;
final RxBool isHoldSpeak = false.obs;
String finalResult = '';
// 按住说话防抖
bool isDebounce = false;
Timer? _talkDebounceTimer;
Timer? _talkStartDebounceTimer;
@override
Future<void> onInit() async {
super.onInit();
@ -90,13 +112,15 @@ class ChatController extends GetxController {
// 获取Agent
final Agent? foundAgent = AgentProvider.getAgentById(agentId);
if (foundAgent == null) {
Logger.error('无法找到Agent: $agentId');
Get.back();
Logger.error('无法找到Agent: $agentId');
return;
}
agent = foundAgent;
// 根据Agent ID选择不同的AI服务
switch (agent.id) {
case 'cyber_girlfriend':
// 亲子陪伴
@ -109,9 +133,12 @@ class ChatController extends GetxController {
_aiService = Get.find<DeapsoundAIService>();
break;
default:
_aiService = Get.find<VolcanoAIService>();
// 默认使用OpenAIServiceAdapter
_aiService = Get.find<OpenAIServiceAdapter>();
Logger.info('使用OpenAIServiceAdapter');
}
Get.put(SpotifService());
_spotifService = Get.find<SpotifService>();
// 使用克隆音色语音合成
final GetStorage storage = GetStorage();
String timbre = storage.read('timbre') ?? '1';
@ -131,8 +158,11 @@ class ChatController extends GetxController {
// 设置TTS服务
await _ttsService.setVoice(agent.voice);
// 加载聊天历史
_loadChatHistory();
// 添加滚动监听器,用于触发加载更多
scrollController.addListener(_scrollListener);
// 加载聊天历史 (加载第一页)
await _loadChatHistory();
// 如果没有历史记录,添加欢迎消息
if (messages.isEmpty && agent.welcomeMessage.isNotEmpty) {
@ -154,8 +184,8 @@ class ChatController extends GetxController {
// 自动停止计时器会在startVoiceRecognition中启动
}
// 订阅VoiceInteractionService事件
_subscribeToVoiceInteractionEvents();
_getLocation();
player.playerStateStream.listen((state) async {
if (state.processingState == ProcessingState.completed) {
@ -171,83 +201,35 @@ class ChatController extends GetxController {
});
}
/// 订阅VoiceInteractionService事件
void _subscribeToVoiceInteractionEvents() {
if (_voiceInteractionService.eventStream != null) {
_voiceInteractionSubscription =
_voiceInteractionService.eventStream.listen((event) {
if (event is RecognitionStartedEvent) {
// 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS
_stopCurrentInteraction();
} else if (event is ChatHistoryEvent) {
// 收到聊天历史事件,添加到聊天记录
_processChatHistoryEvent(event);
}
});
}
}
/// 处理聊天历史事件
void _processChatHistoryEvent(ChatHistoryEvent event) {
Logger.info('收到后台聊天历史事件: agentId=${event.agentId}');
// 如果消息不是空的且不是当前Agent的,不处理
if (event.userMessage.isEmpty || event.agentId != agent.id) {
return;
}
// 添加用户消息
final userMsg = Message(
role: 'user',
content: event.userMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp),
);
// 添加助手消息
final assistantMsg = Message(
role: 'assistant',
content: event.assistantMessage,
timestamp:
DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 助手消息稍后一毫秒
);
// 防止消息重复
if (!_isDuplicateMessage(userMsg) && !_isDuplicateMessage(assistantMsg)) {
messages.add(userMsg);
messages.add(assistantMsg);
}
_scrollToBottom();
void _getLocation() async {
await _amapFlutter.initLocation();
_locationStream = _amapFlutter.locationStream().listen((data) async {
_latLng = '${data['longitude']},${data['latitude']}}';
_city = data['city'];
_amapFlutter.stopLocation();
});
_amapFlutter.startLocation();
}
/// 停止当前交互
void _stopCurrentInteraction() {
Logger.info('收到后台语音识别开始事件,停止当前AI交互');
// 停止当前AI交互
if (isSending.value || _aiResponseSubscription != null) {
_interruptCurrentResponse("收到后台语音识别开始事件,中断当前响应");
}
// 停止TTS
_ttsService.stop();
// 停止语音识别
if (isVoiceRecognitionActive.value) {
stopVoiceRecognition();
}
}
@override
void onClose() {
Logger.info('ChatController onClose');
// 清理资源
scrollController.removeListener(_scrollListener); // 移除监听器
scrollController.dispose();
_aiResponseSubscription?.cancel();
_recognitionSubscription?.cancel();
_voiceInteractionSubscription?.cancel();
_autoStopTimer?.cancel();
_scrollDebounceTimer?.cancel(); // 清理防抖计时器
_talkDebounceTimer?.cancel();
_talkStartDebounceTimer?.cancel();
_amapFlutter.dispose();
_locationStream?.cancel();
// 停止TTS
_ttsService.stop();
@ -258,7 +240,7 @@ class ChatController extends GetxController {
// 保存当前聊天记录
if (agent.id != null) {
_historyService.saveHistory(agent.id!, messages.toList());
// _historyService.saveHistory(agent.id!, messages.toList());
}
player.dispose();
@ -294,11 +276,12 @@ class ChatController extends GetxController {
_aiResponseSubscription!.cancel();
_aiResponseSubscription = null;
// 将加载中的消息转换为正常消息,保留已输出的内容
final index = messages.indexWhere((m) => m.isLoading);
// 将AI中间消息转换为正常消息,保留已输出的内容
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
messages[index] = messages[index].copyWith(
isLoading: false,
isIntermediate: false,
);
messages.refresh();
}
@ -312,7 +295,6 @@ class ChatController extends GetxController {
Future<void> sendMessage(String text) async {
if (text.trim().isEmpty) return;
_voiceInteractionService.pauseVoiceInteraction();
// 中断当前响应
_interruptCurrentResponse("发送新消息,中断当前响应");
@ -324,14 +306,15 @@ class ChatController extends GetxController {
timestamp: DateTime.now(),
);
messages.add(userMessage);
_saveMessageToDb(userMessage);
// 清空输入框
inputText.value = '';
// 添加加载中的消息
final loadingMessage = Message.loading();
messages.add(loadingMessage);
// 添加中间状态的AI消息
final intermediateMessage = Message.intermediate('assistant');
messages.add(intermediateMessage);
messages.refresh();
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
@ -352,47 +335,56 @@ class ChatController extends GetxController {
messages: messageHistory,
systemPrompt: agent.systemPrompt,
);
Logger.info(
'liwei----------- 发送用户提问 ${text}',
);
// 订阅响应流
_aiResponseSubscription = responseStream.listen(
(chunk) {
if (_shouldCancelAiResponse) return;
Logger.info('chat ${chunk}');
if (chunk is Map) {
_updateLoadingMessage(data: chunk);
if (chunk['musicaudio'] != null) {
play(chunk['musicaudio']);
Logger.info(
'liwei-----------收到卡片消息 ${chunk}',
);
_updateIntermediateMessage(meta: chunk);
if (chunk['card_spotify'] != null) {
play_spotify(chunk['card_spotify']);
}
} else {
if (chunk['card_music'] != null) {
play_music(chunk['card_music']);
}
} else if (chunk is String) {
fullResponse += chunk;
// 更新加载中的消息内容
_updateLoadingMessage(content: fullResponse);
// 更新中间状态的消息内容
_updateIntermediateMessage(content: fullResponse);
// 如果TTS启用,使用流式处理发送到TTS
if (isTtsEnabled.value) {
_ttsService.speakStream(chunk);
String str = chunk.replaceAll('*', '');
_ttsService.speakStream(str);
}
}
},
onDone: () {
Logger.info(
'liwei-----------结束当前问题 ${text}',
);
if (_shouldCancelAiResponse) return;
// 完成响应,将isLoading设为false
// 完成响应,将isIntermediate设为false
_finalizeResponse(fullResponse);
// 如果TTS启用,处理缓冲区中的剩余文本
if (isTtsEnabled.value) {
_ttsService.flushStream();
}
// 保存聊天历史
_saveChatHistory();
},
onError: (error) {
Logger.error('AI响应错误', error);
// 更新加载中的消息为错误消息
// 更新中间状态的消息为错误消息
_handleResponseError();
},
);
@ -408,9 +400,13 @@ class ChatController extends GetxController {
final List<Map<String, String>> messageHistory = [];
// 只取最近的10条消息作为上下文
final recentMessages = messages.where((m) => !m.isLoading).toList();
final recentMessages = messages.where((m) => !m.isIntermediate).toList();
final startIndex =
recentMessages.length > 10 ? recentMessages.length - 10 : 0;
messageHistory.add({
'role': 'user',
'content': '我当前所在城市是$_city,我的经纬度是$_latLng',
});
for (int i = startIndex; i < recentMessages.length; i++) {
final m = recentMessages[i];
@ -423,36 +419,33 @@ class ChatController extends GetxController {
return messageHistory;
}
// 更新加载中的消息
void _updateLoadingMessage({Map? data, String? content}) {
final index = messages.indexWhere((m) => m.isLoading);
// 更新中间状态的消息
void _updateIntermediateMessage({Map? meta, String? content}) {
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
if (data != null) {
Map messagesData = messages[index].data ?? {};
if (meta != null) {
Map messagesData = messages[index].meta ?? {};
messages[index] = messages[index].copyWith(
data: {...messagesData, ...data},
isLoading: true,
);
}
if (content != null) {
messages[index] = messages[index].copyWith(
content: content,
isLoading: true,
meta: {...messagesData, ...meta},
);
}
messages[index] = messages[index].copyWith(
content: content,
);
messages.refresh();
_scrollToBottom();
}
}
// 播放音乐
void play(Map musicaudio) async {
void play_music(Map card) async {
try {
isPlay.value = false;
if (audiourl.value != musicaudio['audiourl']) {
if (audiourl.value != card['url']) {
audiourl.value = '';
await player.setUrl(musicaudio['audiourl']);
audiourl.value = musicaudio['audiourl'];
await player.setUrl(card['url']);
audiourl.value = card['url'];
player.play();
} else {
player.play();
@ -463,35 +456,79 @@ class ChatController extends GetxController {
}
}
// 暂停播放音乐
void pause() async {
// 播放音乐
void pause_music() async {
await player.pause();
isPlay.value = false;
}
// 完成响应
// 播放音乐
void play_spotify(Map card) async {
try {
await _spotifService.initialize();
_spotifService.play(card['url'] as String);
isPlay.value = true;
audiourl.value = card['url'] as String;
} catch (e) {
Logger.error('加载异常: $e');
}
}
// 暂停播放音乐
void pause_spotify() async {
try {
_spotifService.pause();
isPlay.value = false;
} catch (e) {
Logger.error('加载异常: $e');
}
}
// 更新或添加最终响应
void _finalizeResponse(String content) {
final index = messages.indexWhere((m) => m.isLoading);
// 查找AI的中间状态消息
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
// 更新中间状态消息为最终状态
messages[index] = messages[index].copyWith(
content: content,
isLoading: false,
isIntermediate: false,
timestamp: DateTime.now(), // 更新时间戳为最终时间
);
messages.refresh();
_scrollToBottom();
// 保存最终的助手消息到数据库
_saveMessageToDb(messages[index]);
} else {
// 如果没有中间状态消息,直接添加新消息 (理论上不应发生)
final assistantMsg = Message(
role: 'assistant',
content: content,
timestamp: DateTime.now(),
);
messages.add(assistantMsg);
_saveMessageToDb(assistantMsg); // 保存到数据库
}
isLoading.value = false;
isSending.value = false;
_aiResponseSubscription = null;
_aiResponseSubscription = null; // 清理订阅
_shouldCancelAiResponse = false; // 重置取消标志
// 响应完成后,重新启动语音识别的自动停止计时器
if (_isVoiceInteractionMode) {
_startAutoStopTimer();
}
}
// 处理响应错误
void _handleResponseError() {
final index = messages.indexWhere((m) => m.isLoading);
// 查找AI的中间状态消息
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
messages[index] = messages[index].copyWith(
content: '抱歉,我遇到了一些问题,无法回应您的请求。',
isLoading: false,
isIntermediate: false,
);
messages.refresh();
}
@ -510,11 +547,79 @@ class ChatController extends GetxController {
}
}
// 按下开始说话
void talkLongPress() async {
_talkStartDebounceTimer?.cancel();
_talkDebounceTimer?.cancel();
isHoldSpeak.value = true;
Haptics.vibrate(HapticsType.success);
_talkStartDebounceTimer =
Timer(const Duration(milliseconds: 260), () async {
if (isDebounce) return;
// 中断当前响应
_interruptCurrentResponse("发送新消息,中断当前响应");
try {
final recognitionStream =
await _asrService.startContinuousRecognition();
await Future.delayed(const Duration(milliseconds: 260));
isDebounce = true;
recognizedText.value = '';
finalResult = '';
// 监听识别事件
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
onError: _handleRecognitionError,
);
} catch (e) {
Logger.error('启动语音识别失败', e);
isHoldSpeak.value = false;
}
});
}
// 松开停止说话
void talkLongPressUp() async {
_talkStartDebounceTimer?.cancel();
_talkDebounceTimer?.cancel();
isHoldSpeak.value = false;
_talkDebounceTimer = Timer(const Duration(milliseconds: 300), () async {
if (!isDebounce) return;
try {
await _asrService.stopContinuousRecognition();
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
String sendText = '';
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
sendText = messages[intermediateIndex].content;
messages.removeAt(intermediateIndex);
}
sendMessage(sendText);
recognizedText.value = '';
finalResult = '';
isDebounce = false;
} catch (e) {
Logger.error('停止语音识别失败', e);
}
});
}
// 开始语音识别
Future<void> startVoiceRecognition() async {
if (isVoiceRecognitionActive.value) return;
_voiceInteractionService.pauseVoiceInteraction();
try {
// 启动连续语音识别
// 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据
@ -524,9 +629,6 @@ class ChatController extends GetxController {
isListening.value = true;
recognizedText.value = '';
// 重置临时消息索引
_tempMessageIndex = -1;
// 启动自动停止计时器
_startAutoStopTimer();
@ -618,24 +720,24 @@ class ChatController extends GetxController {
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
}
// 更新或创建临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
// 更新已有的临时消息
messages[_tempMessageIndex] = messages[_tempMessageIndex].copyWith(
content: text,
// 查找或创建用户的中间状态消息
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
// 更新已有的中间状态消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
content: isVoiceRecognitionActive.value ? text : finalResult + text,
);
messages.refresh();
} else {
// 添加新的临时消息
final tempMessage = Message(
role: 'user',
content: text,
timestamp: DateTime.now(),
);
messages.add(tempMessage);
_tempMessageIndex = messages.length - 1;
// 添加新的中间状态消息
var intermediateMessage = Message.intermediate('user');
intermediateMessage = intermediateMessage.copyWith(content: text);
messages.add(intermediateMessage);
messages.refresh();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
@ -646,27 +748,44 @@ class ChatController extends GetxController {
if (text.isEmpty) return;
Logger.info('处理语音识别最终结果: $text');
recognizedText.value = text;
// 更新活动时间
_updateActivityTime();
// 查找并移除临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
messages.removeAt(_tempMessageIndex);
}
if (isVoiceRecognitionActive.value) {
// 一直语音说话
recognizedText.value = text;
// 查找并移除用户的中间状态消息
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
messages.removeAt(intermediateIndex);
}
// 重置临时消息索引
_tempMessageIndex = -1;
// 发送识别到的文本
sendMessage(text);
// 重置识别文本,但不关闭语音识别框
recognizedText.value = '';
// 重置语音检测状态
isSpeechDetected.value = false;
} else {
// 按住说话
// 发送识别到的文本
sendMessage(text);
finalResult += text;
recognizedText.value = finalResult;
// 重置识别文本,但不关闭语音识别框
recognizedText.value = '';
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
// 重置语音检测状态
isSpeechDetected.value = false;
if (intermediateIndex >= 0) {
// 更新已有的中间状态消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
content: finalResult,
);
messages.refresh();
}
}
}
// 处理语音识别事件错误
@ -685,16 +804,21 @@ class ChatController extends GetxController {
// 清理语音识别资源
void _cleanupRecognition() {
// 查找并移除临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
if (messages[_tempMessageIndex].content.trim().isEmpty) {
messages.removeAt(_tempMessageIndex);
// 查找并移除用户的中间状态消息
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
if (messages[intermediateIndex].content.trim().isEmpty) {
messages.removeAt(intermediateIndex);
} else {
// 如果中间状态消息有内容,将其转换为正式消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
isIntermediate: false,
);
}
messages.refresh();
}
// 重置临时消息索引
_tempMessageIndex = -1;
// 停止语音识别
isVoiceRecognitionActive.value = false;
isListening.value = false;
@ -716,15 +840,21 @@ class ChatController extends GetxController {
recognizedText.value = '';
isSpeechDetected.value = false;
// 查找并移除临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
if (messages[_tempMessageIndex].content.trim().isEmpty) {
messages.removeAt(_tempMessageIndex);
// 查找并处理用户的中间状态消息
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
if (messages[intermediateIndex].content.trim().isEmpty) {
// 如果没有内容则移除
messages.removeAt(intermediateIndex);
} else {
// 有内容则转为正式消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
isIntermediate: false,
);
}
messages.refresh();
}
// 重置临时消息索引
_tempMessageIndex = -1;
} catch (e) {
Logger.error('停止语音识别失败', e);
}
@ -733,7 +863,7 @@ class ChatController extends GetxController {
// 清空聊天记录
void clearChat() {
messages.clear();
_ttsService.stop();
// 添加欢迎消息
if (agent.welcomeMessage.isNotEmpty) {
messages.add(Message(
@ -742,31 +872,121 @@ class ChatController extends GetxController {
timestamp: DateTime.now(),
));
}
// 删除数据库中的历史记录
ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) {
Logger.error('清空数据库聊天记录失败: $e');
});
}
// 删除存储的历史记录
_historyService.clearHistory(agent.id);
// 滚动监听,用于触发加载更多历史记录
void _scrollListener() {
// 当滚动到顶部并且有更多历史记录且不在加载中时,加载更多
if (scrollController.position.pixels ==
scrollController.position.minScrollExtent &&
_hasMoreHistory.value &&
!_isLoadingMore.value) {
_loadMoreHistory();
}
}
// 加载聊天历史
void _loadChatHistory() {
// 从历史服务加载
final history = _historyService.loadHistory(agent.id);
if (history.isNotEmpty) {
messages.clear();
messages.addAll(history);
// 加载聊天历史 (首次加载或刷新)
Future<void> _loadChatHistory() async {
isLoading.value = true;
_currentPage.value = 1; // 重置为第一页
_hasMoreHistory.value = true; // 假设有更多
try {
final pagedResult = await ChatStorage.getMessages(
sessionId: agent.id,
page: _currentPage.value,
pageSize: _pageSize,
);
_totalPages.value = pagedResult.totalPages;
_hasMoreHistory.value = _currentPage.value < _totalPages.value;
final historyMessages = pagedResult.messages.map((chatMsg) {
return Message(
role: chatMsg.sender,
content: chatMsg.message,
timestamp: DateTime.parse(chatMsg.timestamp),
meta: chatMsg.metadata != null ? jsonDecode(chatMsg.metadata!) : null,
);
}).toList();
messages.assignAll(historyMessages.reversed); // 最新的在底部,所以要反转
// 滚动到底部 (如果是首次加载)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
} catch (e) {
Logger.error('加载聊天历史失败', e);
} finally {
isLoading.value = false;
}
}
// 加载更多历史记录
Future<void> _loadMoreHistory() async {
if (_isLoadingMore.value || !_hasMoreHistory.value) return;
_isLoadingMore.value = true;
_currentPage.value++;
try {
final pagedResult = await ChatStorage.getMessages(
sessionId: agent.id,
page: _currentPage.value,
pageSize: _pageSize,
);
_totalPages.value = pagedResult.totalPages;
_hasMoreHistory.value = _currentPage.value < _totalPages.value;
final olderMessages = pagedResult.messages.map((chatMsg) {
return Message(
role: chatMsg.sender,
content: chatMsg.message,
timestamp: DateTime.parse(chatMsg.timestamp),
meta: chatMsg.metadata != null ? jsonDecode(chatMsg.metadata!) : null,
);
}).toList();
// 将旧消息插入到列表顶部
messages.insertAll(0, olderMessages.reversed);
} catch (e) {
Logger.error('加载更多聊天历史失败', e);
_currentPage.value--; // 加载失败,恢复页码
} finally {
_isLoadingMore.value = false;
}
}
// 保存聊天历史
void _saveChatHistory() {
_historyService.saveHistory(agent.id, messages);
// 保存单条聊天消息到数据库
Future<void> _saveMessageToDb(Message message) async {
if (message.isIntermediate) return; // 不保存中间状态的消息
try {
await ChatStorage.saveMessage(
sessionId: agent.id,
message: message.content,
sender: message.role,
metadata: jsonEncode(message.meta),
);
} catch (e) {
Logger.error('保存消息到数据库失败', e);
}
}
// 添加消息,供外部调用
// 添加消息,供外部调用 (如用户发送消息、AI回复)
void addMessage(Message message) {
messages.add(message);
_saveChatHistory();
_scrollToBottom();
_saveMessageToDb(message); // 保存到数据库
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
void _stopVoiceRecognitionAutomatically() {

32
lib/modules/chat/models/message_model.dart

@ -1,16 +1,16 @@
class Message {
final String role; // 'user' or 'assistant'
final String content;
final Map? data;
final Map? meta;
final DateTime timestamp;
final bool isLoading;
final bool isIntermediate; // 中间状态标记,替代isLoading
Message({
required this.role,
required this.content,
required this.timestamp,
this.data,
this.isLoading = false,
this.meta,
this.isIntermediate = false, // 默认不是中间状态
});
// 从JSON构造函数
@ -18,9 +18,9 @@ class Message {
return Message(
role: json['role'] as String,
content: json['content'] as String,
data: json['data'],
meta: json['meta'],
timestamp: DateTime.parse(json['timestamp'] as String),
isLoading: json['isLoading'] as bool? ?? false,
isIntermediate: json['isIntermediate'] as bool? ?? false,
);
}
@ -29,19 +29,19 @@ class Message {
return {
'role': role,
'content': content,
'data': data,
'meta': meta,
'timestamp': timestamp.toIso8601String(),
'isLoading': isLoading,
'isIntermediate': isIntermediate,
};
}
// 创建一个加载中的消息
factory Message.loading() {
// 创建一个中间状态的消息
factory Message.intermediate(String role) {
return Message(
role: 'assistant',
role: role,
content: '',
timestamp: DateTime.now(),
isLoading: true,
isIntermediate: true,
);
}
@ -49,16 +49,16 @@ class Message {
Message copyWith({
String? role,
String? content,
Map? data,
Map? meta,
DateTime? timestamp,
bool? isLoading,
bool? isIntermediate,
}) {
return Message(
role: role ?? this.role,
content: content ?? this.content,
data: data ?? this.data,
meta: meta ?? this.meta,
timestamp: timestamp ?? this.timestamp,
isLoading: isLoading ?? this.isLoading,
isIntermediate: isIntermediate ?? this.isIntermediate,
);
}
}

69
lib/modules/chat/views/animated_gradient_background.dart

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
class AnimatedGradientBackground extends StatefulWidget {
final Widget child;
const AnimatedGradientBackground(this.child, {super.key});
@override
State<AnimatedGradientBackground> createState() =>
_AnimatedGradientBackgroundState();
}
class _AnimatedGradientBackgroundState extends State<AnimatedGradientBackground>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Alignment?> _beginAnimation;
late Animation<Alignment?> _endAnimation;
final Color color1 = const Color(0xFFfbcfdd);
final Color color2 = const Color(0xFFe2e9fb);
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 10),
)..repeat(reverse: true);
// 创建起始点动画
_beginAnimation = AlignmentTween(
begin: Alignment.topCenter,
end: Alignment.bottomLeft,
).animate(_controller);
// 创建结束点动画
_endAnimation = AlignmentTween(
begin: Alignment.bottomCenter,
end: Alignment.topRight,
).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [color1, color2],
begin: _beginAnimation.value!,
end: _endAnimation.value!,
),
),
child: child,
);
},
child: widget.child,
);
}
}

57
lib/modules/chat/views/chat_music_card.dart

@ -5,15 +5,16 @@ import 'package:get/get.dart';
import '../controllers/chat_controller.dart';
class ChatMusicCard extends GetView<ChatController> {
final int type;
final Map? data;
final bool isDarkMode;
const ChatMusicCard(this.data, this.isDarkMode, {super.key});
const ChatMusicCard(this.type, this.data, this.isDarkMode, {super.key});
@override
Widget build(BuildContext context) {
final Map musiccards = data?['musiccards'].first;
final Map musicaudio = data?['musicaudio'];
// final Map musiccards = data?['musiccards'].first;
// final Map musicaudio = data?['musicaudio'];
return Row(
children: [
@ -25,14 +26,14 @@ class ChatMusicCard extends GetView<ChatController> {
borderRadius: BorderRadius.circular(6.r),
),
child: Image.network(
musiccards['image'],
data?['image'],
fit: BoxFit.cover,
),
),
10.horizontalSpace,
Expanded(
child: Text(
musiccards['name'],
data?['name'],
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
@ -43,39 +44,23 @@ class ChatMusicCard extends GetView<ChatController> {
),
10.horizontalSpace,
Obx(
() => controller.audiourl == musicaudio['audiourl'] &&
() => controller.audiourl.value == data?["url"] &&
controller.isPlay.value
? GestureDetector(
onTap: () {
controller.pause();
switch (type) {
case 0:
controller.pause_music();
break;
case 1:
controller.pause_spotify();
break;
default:
}
},
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 38.w,
height: 38.w,
child: StreamBuilder<Duration>(
stream: controller.player.positionStream,
builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero;
final total =
controller.player.duration ?? Duration.zero;
final progress = total.inSeconds > 0
? position.inSeconds / total.inSeconds
: 0.0;
return CircularProgressIndicator(
value: progress,
backgroundColor: isDarkMode
? Colors.grey[700]!
: Colors.grey[300]!,
valueColor: AlwaysStoppedAnimation<Color>(
isDarkMode ? Colors.white : Colors.black),
strokeWidth: 2, // 线条粗细
);
},
),
),
Icon(
Icons.pause_rounded,
color: isDarkMode ? Colors.white : Colors.black,
@ -85,7 +70,15 @@ class ChatMusicCard extends GetView<ChatController> {
)
: GestureDetector(
onTap: () {
controller.play(musicaudio);
switch (type) {
case 0:
controller.play_music(data!);
break;
case 1:
controller.play_spotify(data!);
break;
default:
}
},
child: Container(
width: 38.w,

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save