Browse Source

bg 001

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
d968cb65bf
  1. 1
      .gitignore
  2. 12
      android/app/build.gradle.kts
  3. 28
      android/app/src/main/AndroidManifest.xml
  4. 29
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  5. 77
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  6. 551
      android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt
  7. 324
      android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt
  8. 5
      lib/core/bindings/initial_binding.dart
  9. 57
      lib/data/services/config_service.dart
  10. 1
      pubspec.yaml

1
.gitignore

@ -13,6 +13,7 @@ doc/api/
# dotenv environment variables file
.env*
.env.example
# Avoid committing generated Javascript files:
*.dart.js

12
android/app/build.gradle.kts

@ -50,11 +50,21 @@ android {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
// 添加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")
// 添加legacy-support-v4依赖
implementation("androidx.legacy:legacy-support-v4:1.0.0")
// // 添加火山语音合成SDK依赖
// implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8")

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

@ -33,32 +33,30 @@
android:enableOnBackInvokedCallback="true"
tools:replace="android:label">
<!-- Audio Service -->
<service android:name="com.ryanheise.audioservice.AudioService"
<!-- 添加语音交互服务 -->
<service
android:name=".VoiceInteractionService"
android:foregroundServiceType="mediaPlayback"
android:exported="true"
android:enabled="true">
<intent-filter>
<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="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<!-- 添加媒体按钮接收器 -->
<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>
<!-- 添加通知服务 -->
<service
android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
android:exported="false"
android:stopWithTask="false"
android:foregroundServiceType="mediaPlayback" />
<activity
android:name=".MainActivity"
android:exported="true"

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

@ -215,4 +215,33 @@ class AzureAsrHelper {
fun onCanceled(reason: String, errorDetails: String)
fun onError(error: String)
}
// 开始连续语音识别(简化版,兼容旧代码)
fun startContinuousRecognition(callback: RecognizeCallback) {
startContinuousRecognition(null, object : ContinuousRecognizeCallback {
override fun onResult(result: String) {
callback.onResult(result)
}
override fun onRecognizing(recognizing: String) {
// 不处理中间结果
}
override fun onSessionStarted() {
// 不处理会话开始事件
}
override fun onSessionStopped() {
// 不处理会话结束事件
}
override fun onCanceled(reason: String, errorDetails: String) {
callback.onError("识别取消: $reason, 详情: $errorDetails")
}
override fun onError(error: String) {
callback.onError(error)
}
})
}
}

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

@ -13,23 +13,33 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.EventChannel
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity
import android.content.Context
import android.content.SharedPreferences
class MainActivity: AudioServiceActivity() {
class MainActivity: FlutterActivity() {
private val AUDIO_PROCESSING_CHANNEL = "com.deep_voice.audio_processing"
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 CONFIG_CHANNEL = "com.example.deep_voice/config"
private val TAG = "MainActivity"
private lateinit var azureAsrHelper: AzureAsrHelper
private lateinit var azureTtsHelper: AzureTtsHelper
private var azureAsrEventSink: EventChannel.EventSink? = null
private lateinit var sharedPreferences: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper()
sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
// 自动启动语音交互服务
startVoiceInteractionService()
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
@ -266,6 +276,55 @@ class MainActivity: AudioServiceActivity() {
}
}
}
// 语音交互服务通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"startVoiceInteractionService" -> {
startVoiceInteractionService()
result.success(true)
}
"stopVoiceInteractionService" -> {
stopVoiceInteractionService()
result.success(true)
}
"isVoiceInteractionServiceRunning" -> {
result.success(VoiceInteractionService.isServiceRunning())
}
else -> {
result.notImplemented()
}
}
}
// 设置 MethodChannel 处理程序
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CONFIG_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"setConfig" -> {
val azureSpeechKey = call.argument<String>("azure_speech_key") ?: ""
val azureSpeechRegion = call.argument<String>("azure_speech_region") ?: ""
val volcanoAiApiKey = call.argument<String>("volcano_ai_api_key") ?: ""
// 保存到 SharedPreferences
val editor = sharedPreferences.edit()
editor.putString("flutter.azure_speech_key", azureSpeechKey)
editor.putString("flutter.azure_speech_region", azureSpeechRegion)
editor.putString("flutter.volcano_ai_api_key", volcanoAiApiKey)
editor.apply()
// 打印日志
println("已保存配置到原生 SharedPreferences:")
println("azure_speech_key: $azureSpeechKey")
println("azure_speech_region: $azureSpeechRegion")
println("volcano_ai_api_key: $volcanoAiApiKey")
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
}
// ASR 事件发送方法
@ -295,6 +354,22 @@ class MainActivity: AudioServiceActivity() {
return true
}
/**
* 启动语音交互服务
*/
private fun startVoiceInteractionService() {
val serviceIntent = Intent(this, VoiceInteractionService::class.java)
startService(serviceIntent)
}
/**
* 停止语音交互服务
*/
private fun stopVoiceInteractionService() {
val serviceIntent = Intent(this, VoiceInteractionService::class.java)
stopService(serviceIntent)
}
override fun onDestroy() {
azureAsrHelper.dispose()
azureTtsHelper.dispose()

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

@ -0,0 +1,551 @@
package com.example.deep_voice
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 java.util.concurrent.atomic.AtomicBoolean
import org.json.JSONArray
/**
* 后台语音交互 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 val isRunning = AtomicBoolean(false)
// 获取服务运行状态
fun isServiceRunning(): Boolean {
return isRunning.get()
}
}
private lateinit var mediaSession: MediaSessionCompat
private var isRecording = false
private var isProcessing = false
private var lastKeyEventTime = 0L
private var keyEventCount = 0
private lateinit var audioManager: AudioManager
// Azure服务
private lateinit var azureAsrHelper: AzureAsrHelper
private lateinit var azureTtsHelper: AzureTtsHelper
// 火山AI服务
private lateinit var volcanoAIService: VolcanoAIService
// 系统提示词
private val systemPrompt = """
你是一个智能语音助手,能够简洁明了地回答用户的问题。
请保持回答简短、准确,避免过长的解释。
如果用户的问题不清楚,请礼貌地请求澄清。
不要使用复杂的术语,除非用户明确要求。
""".trimIndent()
override fun onCreate() {
super.onCreate()
Log.d(TAG, "onCreate")
isRunning.set(true)
// 获取 AudioManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 1. 初始化Azure服务
initAzureServices()
// 2. 初始化火山AI服务
volcanoAIService = VolcanoAIService(applicationContext)
// 3. 初始化MediaSession
initMediaSession()
// 4. 启动前台通知(Foreground Service)
startForegroundServiceWithNotification()
// 5. 注册成为媒体按钮接收器
registerMediaButtonReceiver()
// 6. 测试按键事件
testKeyEvent()
}
/**
* 测试按键事件,模拟一个按键事件以验证处理逻辑
*/
private fun testKeyEvent() {
Log.d(TAG, "测试按键事件处理")
val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK)
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
putExtra(Intent.EXTRA_KEY_EVENT, keyEvent)
component = ComponentName(packageName, VoiceInteractionService::class.java.name)
}
// MediaButtonReceiver.handleIntent 返回的是 KeyEvent 类型,而不是 Boolean
MediaButtonReceiver.handleIntent(mediaSession, mediaButtonIntent)
Log.d(TAG, "测试按键事件已发送")
// 直接处理按键事件
handleKeyEvent(keyEvent)
}
/**
* 处理按键事件
*/
private fun handleKeyEvent(keyEvent: KeyEvent) {
if (keyEvent.action == KeyEvent.ACTION_DOWN) {
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PAUSE,
KeyEvent.KEYCODE_HEADSETHOOK,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(TAG, "处理按键事件: ${keyEvent.keyCode}")
// 检测双击
val currentTime = System.currentTimeMillis()
if (currentTime - lastKeyEventTime < 500) { // 500毫秒内的按键视为连续按键
keyEventCount++
if (keyEventCount == 1) { // 这是第二次按键,即双击
Log.d(TAG, "检测到双击,开始语音交互")
handleDoubleClick()
keyEventCount = 0
}
} else {
keyEventCount = 0
}
lastKeyEventTime = currentTime
}
}
}
}
/**
* 注册成为媒体按钮接收器
*/
private fun registerMediaButtonReceiver() {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver)
Log.d(TAG, "已注册媒体按钮接收器: $mediaButtonReceiver")
}
/**
* 初始化Azure服务
*/
private fun initAzureServices() {
azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper()
// 从应用的SharedPreferences获取Azure配置
val sharedPreferences = applicationContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val subscriptionKey = sharedPreferences.getString("flutter.azure_speech_key", "") ?: ""
val serviceRegion = sharedPreferences.getString("flutter.azure_speech_region", "") ?: ""
Log.d(TAG, "Azure配置信息: subscriptionKey=$subscriptionKey, serviceRegion=$serviceRegion")
if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) {
// 初始化ASR
azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN")
// 初始化TTS
azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN")
} else {
Log.e(TAG, "Azure配置信息不完整,无法初始化Azure服务")
}
}
/**
* 初始化 MediaSession 以捕获蓝牙耳机按键
*/
private fun initMediaSession() {
mediaSession = MediaSessionCompat(applicationContext, "VoiceServiceSession")
// 支持媒体按钮和传输控件
mediaSession.setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
// 设置播放状态
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
)
.setState(PlaybackStateCompat.STATE_PAUSED, 0, 1.0f)
mediaSession.setPlaybackState(playbackStateBuilder.build())
// 监听回调
mediaSession.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
// 根据 Android 版本使用不同的方式获取 KeyEvent
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
}
Log.d(TAG, "收到媒体按钮事件: $keyEvent")
if (keyEvent != null) {
Log.d(TAG, "按键代码: ${keyEvent.keyCode}, 动作: ${keyEvent.action}")
if (keyEvent.action == KeyEvent.ACTION_DOWN) {
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PAUSE,
KeyEvent.KEYCODE_HEADSETHOOK,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(TAG, "蓝牙耳机按键被按下: ${keyEvent.keyCode}")
// 检测双击
val currentTime = System.currentTimeMillis()
if (currentTime - lastKeyEventTime < 500) { // 500毫秒内的按键视为连续按键
keyEventCount++
if (keyEventCount == 1) { // 这是第二次按键,即双击
Log.d(TAG, "检测到双击,开始语音交互")
handleDoubleClick()
keyEventCount = 0
return true
}
} else {
keyEventCount = 0
}
lastKeyEventTime = currentTime
// 单击也处理
if (keyEventCount == 0) {
Log.d(TAG, "检测到单击,开始语音交互")
handleDoubleClick()
return true
}
}
}
}
}
return super.onMediaButtonEvent(mediaButtonEvent)
}
override fun onPlay() {
Log.d(TAG, "MediaSession.onPlay()")
handleDoubleClick()
return super.onPlay()
}
override fun onPause() {
Log.d(TAG, "MediaSession.onPause()")
handleDoubleClick()
return super.onPause()
}
})
// 激活 MediaSession
mediaSession.isActive = true
Log.d(TAG, "MediaSession 已激活: ${mediaSession.isActive}")
}
/**
* 处理耳机双击事件
*/
private fun handleDoubleClick() {
if (isProcessing) {
Log.d(TAG, "正在处理中,忽略此次点击")
return
}
if (!isRecording) {
startVoiceInteraction()
} else {
stopVoiceInteraction()
}
}
/**
* 开始语音交互
*/
private fun startVoiceInteraction() {
if (isRecording || isProcessing) {
return
}
isRecording = true
isProcessing = true
// 播放提示音
playStartPrompt()
// 开始录音识别
azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String) {
// 实时识别结果,可以记录日志
Log.d(TAG, "正在识别: $recognizing")
}
override fun onResult(result: String) {
// 最终识别结果
Log.d(TAG, "语音识别结果: $result")
// 停止录音
isRecording = false
// 如果有识别结果,则发送到VolcanoAI处理
if (result.isNotEmpty()) {
processWithVolcanoAI(result)
} else {
isProcessing = false
}
}
override fun onSessionStarted() {
Log.d(TAG, "语音识别会话开始")
}
override fun onSessionStopped() {
Log.d(TAG, "语音识别会话结束")
}
override fun onCanceled(reason: String, errorDetails: String) {
Log.e(TAG, "语音识别取消: $reason, 详情: $errorDetails")
isRecording = false
isProcessing = false
}
override fun onError(error: String) {
Log.e(TAG, "语音识别错误: $error")
isRecording = false
isProcessing = false
// 播放错误提示音
playErrorPrompt()
}
})
}
/**
* 停止语音交互
*/
private fun stopVoiceInteraction() {
if (!isRecording) {
return
}
// 停止录音
azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(result: String) {
// 不处理结果
}
override fun onRecognizing(recognizing: String) {
// 不处理中间结果
}
override fun onSessionStarted() {
// 不处理会话开始事件
}
override fun onSessionStopped() {
Log.d(TAG, "语音识别会话已停止")
}
override fun onCanceled(reason: String, errorDetails: String) {
Log.d(TAG, "语音识别已取消: $reason")
}
override fun onError(error: String) {
Log.e(TAG, "停止语音识别错误: $error")
}
})
isRecording = false
}
/**
* 播放开始提示音
*/
private fun playStartPrompt() {
azureTtsHelper.speakText("我在听", object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
Log.d(TAG, "提示音播放完成")
}
override fun onError(error: String) {
Log.e(TAG, "提示音播放错误: $error")
}
})
}
/**
* 播放错误提示音
*/
private fun playErrorPrompt() {
azureTtsHelper.speakText("抱歉,我没有听清", object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
Log.d(TAG, "错误提示音播放完成")
}
override fun onError(error: String) {
Log.e(TAG, "错误提示音播放错误: $error")
}
})
}
/**
* 使用VolcanoAI处理语音识别结果
*/
private fun processWithVolcanoAI(text: String) {
// 使用原生VolcanoAIService处理
Thread {
try {
// 创建用户消息
val messages = JSONArray().apply {
put(volcanoAIService.createUserMessage(text))
}
// 发送请求到VolcanoAI
val response = volcanoAIService.sendMessage(messages, systemPrompt)
// 播放AI回复
speakAIResponse(response)
} catch (e: Exception) {
Log.e(TAG, "VolcanoAI处理错误: ${e.message}")
isProcessing = false
// 播放错误提示音
playErrorPrompt()
}
}.start()
}
/**
* 播放AI回复
*/
private fun speakAIResponse(text: String) {
azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
Log.d(TAG, "AI回复播放完成")
isProcessing = false
}
override fun onError(error: String) {
Log.e(TAG, "AI回复播放错误: $error")
isProcessing = false
}
})
}
/**
* 启动一个"前台通知",保证此Service在后台不会被系统回收
*/
private fun startForegroundServiceWithNotification() {
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
)
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("语音助手")
.setContentText("双击蓝牙耳机按键即可开始语音交互")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.build()
startForeground(NOTIFICATION_ID, notification)
}
/**
* 当 Service 被启动时调用
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand: $intent")
// 处理媒体按键 Intent
if (intent != null) {
// 如果是媒体按钮事件,尝试手动处理
if (Intent.ACTION_MEDIA_BUTTON == intent.action) {
Log.d(TAG, "收到媒体按钮事件: $intent")
// 根据 Android 版本使用不同的方式获取 KeyEvent
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
}
Log.d(TAG, "手动处理媒体按钮事件: $keyEvent")
if (keyEvent != null) {
handleKeyEvent(keyEvent)
}
}
// 无论如何都要尝试让 MediaSession 处理
MediaButtonReceiver.handleIntent(mediaSession, intent)
}
return START_STICKY
}
@Nullable
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
Log.d(TAG, "onDestroy")
super.onDestroy()
// 取消注册媒体按钮接收器
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver)
// 释放资源
mediaSession.isActive = false
mediaSession.release()
// 释放Azure服务
azureAsrHelper.dispose()
azureTtsHelper.dispose()
isRunning.set(false)
}
}

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

@ -0,0 +1,324 @@
package com.example.deep_voice
import android.content.Context
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 context: Context) {
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 val apiKey: String
get() {
val sharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
return sharedPreferences.getString("flutter.volcano_ai_api_key", "") ?: ""
}
init {
if (apiKey.isEmpty()) {
Log.e(TAG, "火山AI配置信息不完整,请检查环境变量")
}
}
/**
* 生成个性化问候语
*
* @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 {
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) {
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)

5
lib/core/bindings/initial_binding.dart

@ -3,11 +3,14 @@ import '../../data/services/azure_asr_service.dart';
import '../../data/services/azure_tts_service.dart';
import '../../data/services/chat_history_service.dart';
import '../../data/services/volcano_translation_service.dart';
import '../../data/services/config_service.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@override
void dependencies() {
// 配置服务 - 需要首先初始化
Get.putAsync<ConfigService>(() => ConfigService().init());
// Azure 语音识别服务
Get.lazyPut<AzureAsrService>(() => AzureAsrService(), fenix: true);
@ -20,8 +23,6 @@ class InitialBinding extends Bindings {
// 火山翻译服务
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(), fenix: true);
}
}

57
lib/data/services/config_service.dart

@ -0,0 +1,57 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:logger/logger.dart';
import 'package:flutter/services.dart';
/// 配置服务,用于管理应用配置和环境变量
class ConfigService extends GetxService {
final Logger _logger = Logger();
late final SharedPreferences _prefs;
// 创建一个 MethodChannel 用于与原生代码通信
static const MethodChannel _channel = MethodChannel('com.example.deep_voice/config');
// 初始化服务
Future<ConfigService> init() async {
try {
_prefs = await SharedPreferences.getInstance();
// 从 .env 读取 Azure 语音服务配置
final azureSpeechKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'];
final azureSpeechRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'];
// 从 .env 读取火山 AI API Key
final volcanoAiApiKey = dotenv.env['VOLCANO_AI_API_KEY'];
// 通过 MethodChannel 向原生代码传递配置值
try {
await _channel.invokeMethod('setConfig', {
'azure_speech_key': azureSpeechKey ?? '',
'azure_speech_region': azureSpeechRegion ?? '',
'volcano_ai_api_key': volcanoAiApiKey ?? '',
});
_logger.i('已通过 MethodChannel 向原生代码传递配置值');
} catch (e) {
_logger.e('通过 MethodChannel 传递配置值失败: $e');
}
return this;
} catch (e) {
_logger.e('初始化配置服务失败: $e');
rethrow;
}
}
// 获取配置值
String? getString(String key) {
return _prefs.getString(key);
}
// 设置配置值
Future<bool> setString(String key, String value) {
return _prefs.setString(key, value);
}
}

1
pubspec.yaml

@ -29,6 +29,7 @@ dependencies:
crypto: ^3.0.3
flutter_tts: ^4.2.2
logger: ^2.0.2+1
shared_preferences: ^2.2.2
# azure_speech_recognition_null_safety: ^0.9.6
window_size:
git:

Loading…
Cancel
Save