liwei1dao 1 year ago
parent
commit
5b7e332c9f
  1. 1
      android/app/src/main/AndroidManifest.xml
  2. 31
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt
  3. 12
      lib/data/services/spotify_service.dart
  4. 37
      lib/modules/chat/controllers/chat_controller.dart
  5. 6
      lib/modules/chat/views/animated_gradient_background.dart
  6. 17
      lib/modules/chat/views/chat_view.dart
  7. 2
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  8. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  9. 4
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties
  10. 25
      local_plugins/opus/example/lib/main.dart
  11. 86
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkHelper.kt
  12. 1
      pubspec.yaml

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

@ -49,6 +49,7 @@
<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="DeapSound"

31
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt

@ -12,6 +12,8 @@ import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.open_ai_service.SystemFunctionHandler
import de.minimalme.spotify_sdk.SpotifySdkHelper
import kotlinx.serialization.json.JsonObject
/**
@ -38,7 +40,9 @@ class VoiceInteractionHandler(
// OpenAI服务
private val openAIService = OpenAIService(context.applicationContext)
private var spotifyHelper: SpotifySdkHelper? = null
// 当前用户输入
private var currentUserInput = ""
@ -271,6 +275,17 @@ class VoiceInteractionHandler(
override fun onFunctionCall(call: JSONObject) {
FileLogger.d(TAG, "processWithOpenAI 收到函数调用请求: ${call.getString("name")}")
// Log.e("OpenAIServicePlugin", "context:${call.toString()}")
// val context = call["context"]
// if (call.has("meta")) {
// val meta = call.get("meta")
// val metaJsonObject: JsonObject = meta as JsonObject
// Log.e("OpenAIServicePlugin", "工具调用结果 context:${context} meta:${meta.toString()}")
// if (metaJsonObject.containsKey("card_spotify")) {
// val card_spotify:JsonObject = metaJsonObject.get("card_spotify") as JsonObject
// playSpotifyMusic(card_spotify.get("name") as String)
// }
// }
}
}
)
@ -314,7 +329,19 @@ class VoiceInteractionHandler(
notifyStateChanged()
}
}
/*
* 播放音乐。Spotify
* */
fun playSpotifyMusic(url:String){
if (spotifyHelper == null){
spotifyHelper = SpotifySdkHelper(context).apply {
initialize(spotifyclientid,spotifyRedirectUrl)
}
}
spotifyHelper?.play(url)
}
/**
* 播放AI回复
*/

12
lib/data/services/spotify_service.dart

@ -20,15 +20,17 @@ class SpotifService {
/// 初始化OpenAI服务
Future<bool> initialize() async {
if (!isInitialized) {
await getAccessToken();
await connectToSpotifyRemote();
isInitialized = true;
final _env = _storage.read("ENV") as Map<String, String>;
SPOTIFY_CLIENT_ID = _env['SPOTIFY_CLIENT_ID'] ?? '';
SPOTIFY_REDIRECT_URL = _env['SPOTIFY_REDIRECT_URL'] ?? '';
printInfo(info: "SpotifService----------- initialize");
}
printInfo(
info:
"SpotifService----------- initialize SPOTIFY_CLIENT_ID:${SPOTIFY_CLIENT_ID} SPOTIFY_REDIRECT_URL:${SPOTIFY_REDIRECT_URL}");
await getAccessToken();
await connectToSpotifyRemote();
isInitialized = true;
}
return true;
}

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

@ -5,6 +5,7 @@ import 'package:deep_voice/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';
@ -112,8 +113,9 @@ 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;
}
@ -402,18 +404,23 @@ 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) {
Logger.info(
'liwei-----------收到卡片消息 ${chunk}',
);
_updateIntermediateMessage(meta: chunk);
if (chunk['card_spotify'] != null) {
play(chunk['card_spotify']);
}
} else {
} else if (chunk is String) {
fullResponse += chunk;
// 更新中间状态的消息内容
@ -421,11 +428,15 @@ class ChatController extends GetxController {
// 如果TTS启用,使用流式处理发送到TTS
if (isTtsEnabled.value) {
_ttsService.speakStream(chunk);
String str = chunk.replaceAll('*', '');
_ttsService.speakStream(str);
}
}
},
onDone: () {
Logger.info(
'liwei-----------结束当前问题 ${text}',
);
if (_shouldCancelAiResponse) return;
// 完成响应,将isIntermediate设为false
@ -458,6 +469,10 @@ class ChatController extends GetxController {
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];
@ -465,12 +480,6 @@ class ChatController extends GetxController {
'role': m.role,
'content': m.content,
});
if (i == recentMessages.length - 2) {
messageHistory.add({
'role': 'user',
'content': '当需要获取我的位置时,我当前在$_city,我的经纬度是$_latLng',
});
}
}
return messageHistory;
@ -586,6 +595,8 @@ class ChatController extends GetxController {
if (isHoldSpeak.value) return;
_voiceInteractionService.pauseVoiceInteraction();
// 中断当前响应
_interruptCurrentResponse("发送新消息,中断当前响应");
try {
final recognitionStream = await _asrService.startContinuousRecognition();
@ -594,6 +605,7 @@ class ChatController extends GetxController {
isHoldSpeak.value = true;
recognizedText.value = '';
finalResult = '';
Haptics.vibrate(HapticsType.success);
// 监听识别事件
_recognitionSubscription = recognitionStream.listen(
@ -616,13 +628,16 @@ class ChatController extends GetxController {
_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(finalResult);
sendMessage(sendText);
recognizedText.value = '';
finalResult = '';

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

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
class AnimatedGradientBackground extends StatefulWidget {
const AnimatedGradientBackground({super.key});
final Widget child;
const AnimatedGradientBackground(this.child, {super.key});
@override
State<AnimatedGradientBackground> createState() =>
@ -58,8 +60,10 @@ class _AnimatedGradientBackgroundState extends State<AnimatedGradientBackground>
end: _endAnimation.value!,
),
),
child: child,
);
},
child: widget.child,
);
}
}

17
lib/modules/chat/views/chat_view.dart

@ -513,18 +513,23 @@ class ChatView extends GetView<ChatController> {
// 构建语音输入面板
Widget _buildVoiceInputPanel(bool isDarkMode) {
return Container(
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
),
child: SafeArea(
return AnimatedGradientBackground(
SafeArea(
child: Column(
children: [
Row(
children: [],
),
Expanded(
child: AnimatedGradientBackground(),
child: Container(
width: 200.w,
height: 200.w,
decoration: BoxDecoration(
color:
isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white,
shape: BoxShape.circle,
),
),
),
SizedBox(
height: 80.h,

2
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt

@ -168,7 +168,7 @@ class MCPSubClient(
private fun processToolsList(toolsResult: ListToolsResult?) {
try {
// 将MCP工具列表转换为ToolUnion列表
tools = toolsResult?.tools?.filter { tool -> config.tools == null || config.tools.contains(tool.name) == true
tools = toolsResult?.tools?.filter { tool -> config.tools.isNullOrEmpty() || config.tools.contains(tool.name)
}?.map { tool ->
// 转换为ToolUnion格式
ToolUnion.ofTool(

BIN
local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

4
local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

@ -1,2 +1,2 @@
#Tue Apr 15 09:36:50 IST 2025
gradle.version=8.10
#Thu Apr 24 10:52:53 CST 2025
gradle.version=8.9

25
local_plugins/opus/example/lib/main.dart

@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:opus/opus.dart';
import '../../lib/opus.dart';
void main() {
runApp(const OpusExampleApp());
@ -92,13 +93,13 @@ class _OpusExamplePageState extends State<OpusExamplePage> {
const int frequency = 440; // 440Hz - A4音符
const int durationMs = 20; // 20ms,按照常规frameSize=960
const int frameSize = 960; // 对应20ms@48kHz
// 创建PCM数据缓冲区,16位有符号整数,每采样2字节,双声道
final int samplesToGenerate = (sampleRate * durationMs / 1000).round();
final int bufferSize = samplesToGenerate * channels * 2; // 2字节/样本
final byteBuffer = ByteData(bufferSize);
// 生成正弦波
for (int i = 0; i < samplesToGenerate; i++) {
// 计算当前时间点
@ -107,18 +108,17 @@ class _OpusExamplePageState extends State<OpusExamplePage> {
final double amplitude = sin(2 * 3.14159265359 * frequency * t);
// 缩放到16位有符号整数范围 (-32768 to 32767)
final int sample = (amplitude * 32767).round();
// 为所有声道写入相同的值
for (int channel = 0; channel < channels; channel++) {
final int offset = (i * channels + channel) * 2;
byteBuffer.setInt16(offset, sample, Endian.little);
}
}
_samplePcmData = Uint8List.fromList(
List.generate(bufferSize, (index) => byteBuffer.getUint8(index))
);
List.generate(bufferSize, (index) => byteBuffer.getUint8(index)));
setState(() {
_statusMessage = '已创建${_samplePcmData!.length}字节测试PCM数据';
});
@ -272,6 +272,9 @@ class _OpusExamplePageState extends State<OpusExamplePage> {
String _formatBytes(Uint8List data) {
final length = data.length > 16 ? 16 : data.length;
return data.sublist(0, length).map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ');
return data
.sublist(0, length)
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join(' ');
}
}
}

86
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkService.kt → local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkHelper.kt

@ -2,7 +2,6 @@ package de.minimalme.spotify_sdk
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.util.Log
import com.spotify.android.appremote.api.ConnectionParams
import com.spotify.android.appremote.api.Connector
@ -20,46 +19,33 @@ import com.spotify.android.appremote.api.error.UserNotAuthorizedException
import com.spotify.sdk.android.auth.AuthorizationClient
import com.spotify.sdk.android.auth.AuthorizationRequest
import com.spotify.sdk.android.auth.AuthorizationResponse
import de.minimalme.spotify_sdk.subscriptions.CapabilitiesChannel
import de.minimalme.spotify_sdk.subscriptions.ConnectionStatusChannel
import de.minimalme.spotify_sdk.subscriptions.PlayerContextChannel
import de.minimalme.spotify_sdk.subscriptions.PlayerStateChannel
import de.minimalme.spotify_sdk.subscriptions.UserStatusChannel
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import kotlinx.event.SetEvent
import kotlinx.event.event
class SpotifySdkService(private val context: Context? = null) {
class SpotifySdkHelper(private val context: Context? = null) {
// application context
private var applicationContext : Context? = null
private var applicationActivity : Activity? = null
// method channel
private lateinit var methodChannel : MethodChannel
private val channelName = "spotify_sdk"
private val loggingTag = "spotify_sdk"
private val loggingTag = "spotify_sdk"
private val errorConnecting = "errorConnecting"
private val errorDisconnecting = "errorDisconnecting"
private val errorConnection = "errorConnection"
private val errorAuthenticationToken = "authenticationTokenError"
private val requestCodeAuthentication = 1337
private var spotifyAppRemote: SpotifyAppRemote? = null
private var playerApi: PlayerApi? = null
//-- Method implementations
private fun connectToSpotify(clientId: String?, redirectUrl: String?, result: MethodChannel.Result) {
fun initialize(clientId: String, redirectUrl: String): Boolean {
getAccessToken(clientId,redirectUrl,"");
connectToSpotify(clientId,redirectUrl)
return true;
}
public fun connectToSpotify(clientId: String?, redirectUrl: String?) {
if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) {
result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "")
// result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "")
} else {
val connectionParams = ConnectionParams.Builder(clientId)
.setRedirectUri(redirectUrl)
@ -75,7 +61,7 @@ class SpotifySdkService(private val context: Context? = null) {
initiallyConnected = true
Log.i(loggingTag, "App Remote successfully connected")
result.success(true)
// result.success(true)
}
override fun onFailure(throwable: Throwable) {
@ -155,25 +141,23 @@ class SpotifySdkService(private val context: Context? = null) {
// )
} else {
// throw exception as the connect method
result.error(errorCode, errorMessage, errorDetails)
// result.error(errorCode, errorMessage, errorDetails)
}
}
})
}
}
private fun getAccessToken(clientId: String?, redirectUrl: String?, scope: String?, result: MethodChannel.Result) {
public fun getAccessToken(clientId: String?, redirectUrl: String?, scope: String?) {
if (applicationActivity == null) {
throw IllegalStateException("getAccessToken needs a foreground activity")
}
if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) {
result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "")
// result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "")
} else {
//Convert String? scope to Array. Delimiter set as comma ","
val scopeArray = scope?.split(",")?.toTypedArray()
// methodConnectToSpotify.checkAndSetPendingOperation(result)
val builder = AuthorizationRequest.Builder(
clientId,
AuthorizationResponse.Type.TOKEN,
@ -190,38 +174,18 @@ class SpotifySdkService(private val context: Context? = null) {
}
}
private fun disconnectFromSpotify(result: MethodChannel.Result) {
public fun disconnectFromSpotify() {
if (spotifyAppRemote != null && spotifyAppRemote!!.isConnected) {
SpotifyAppRemote.disconnect(spotifyAppRemote)
// emit connection terminated event
// connStatusEventChannel(
// ConnectionStatusChannel.ConnectionEvent(
// false,
// "Successfully disconnected from Spotify.",
// null,
// null
// )
// )
// method success
result.success(true)
// result.success(true)
} else if (!spotifyAppRemote!!.isConnected) {
result.error(errorDisconnecting, "could not disconnect spotify remote", "you are not connected, no need to disconnect")
// result.error(errorDisconnecting, "could not disconnect spotify remote", "you are not connected, no need to disconnect")
} else {
result.error(errorDisconnecting, "could not disconnect spotify remote", "spotifyAppRemote is not set")
// result.error(errorDisconnecting, "could not disconnect spotify remote", "spotifyAppRemote is not set")
}
}
private fun play(spotifyUri: String?){
// if (playerApi != null && !spotifyUri.isNullOrBlank()) {
// playerApi.play(spotifyUri)
// .setResultCallback { result.success(true) }
// .setErrorCallback { throwable -> result.error(errorPlay, "error when playing uri: $spotifyUri", throwable.toString()) }
// } else if (spotifyUri.isNullOrBlank()) {
// result.error(errorPlay, "spotifyUri has invalid format or is not set", "")
// } else {
// spotifyRemoteAppNotSetError()
// }
public fun play(spotifyUri: String?){
if (playerApi != null && !spotifyUri.isNullOrBlank()) {
playerApi!!.play(spotifyUri)
} else if (spotifyUri.isNullOrBlank()) {
@ -230,14 +194,8 @@ class SpotifySdkService(private val context: Context? = null) {
}
}
private fun pause(){
// if (playerApi != null) {
// playerApi.pause()
// .setResultCallback { result.success(true) }
// .setErrorCallback { throwable -> result.error(errorPause, "error when pausing", throwable.toString()) }
// } else {
// spotifyRemoteAppNotSetError()
// }
public fun pause(){
if (playerApi != null) {
playerApi!!.pause()
.setResultCallback {}

1
pubspec.yaml

@ -35,6 +35,7 @@ dependencies:
flutter_easyloading: ^3.0.5
package_info_plus: ^8.3.0
file_picker: ^8.0.0+1
haptic_feedback: ^0.5.1+1
window_size:
git:

Loading…
Cancel
Save