Просмотр исходного кода

feat(asr): 优化语音识别功能并添加话题管理支持

- 添加了自定义语音识别对话框界面,支持黑色背景和绿色圆角边框
- 实现了场景切换功能,decision场景下可直接进行聊天而无需意图识别
- 新增TopicData数据模型用于话题管理
- 集成tbtopic接口实现话题ID获取功能
- 更新ChatActivity在进入时预创建话题
- 修正欢迎界面唤醒词提示文本为"飞宝飞宝"
- 重构ASR回调处理逻辑,添加onDirectChat回调支持
徐勤民 14 часов назад
Родитель
Сommit
5f44916c90

+ 41 - 12
app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt

@@ -1,10 +1,15 @@
 package com.nova.brain.glass.helper
 
+import android.R.attr.action
 import android.app.AlertDialog
+import android.graphics.Color
+import android.graphics.drawable.GradientDrawable
 import android.os.Handler
 import android.os.Looper
 import android.util.Log
+import android.view.LayoutInflater
 import com.nova.brain.glass.BuildConfig
+import com.nova.brain.glass.R
 import com.nova.brain.glass.model.RecognizeAction
 import com.rokid.online.speech.AsrClient
 import com.rokid.online.speech.OnlineSpeechSdk
@@ -57,6 +62,9 @@ object AsrHelper : OfflineCmdListener {
     /** goToDecisionCenter 命中时的回调,由各 Activity 在 onResume/onPause 中注册/清空 */
     var onGoToDecisionCenter: ((action: RecognizeAction) -> Unit)? = null
 
+    /** scene == "decision" 时直接用 ASR 文本发起对话,由 ChatActivity 注册 */
+    var onDirectChat: ((text: String) -> Unit)? = null
+
     fun init() {
         val cfg = OnlineSpeechSdkConfig(
             domain = DOMAIN,
@@ -121,11 +129,28 @@ object AsrHelper : OfflineCmdListener {
             val activity = runCatching { AppManager.getInstance().getActivity() }.getOrNull()
                 ?: return@post
             if (activity.isFinishing || activity.isDestroyed) return@post
+
+            val contentView = LayoutInflater.from(activity)
+                .inflate(R.layout.dialog_listening, null)
+
+            // 黑底 + 绿色圆角线框
+            val density = activity.resources.displayMetrics.density
+            contentView.background = GradientDrawable().apply {
+                shape = GradientDrawable.RECTANGLE
+                cornerRadius = 12f * density
+                setColor(Color.BLACK)
+                setStroke((2f * density).toInt(), Color.parseColor("#FF40FF5E"))
+            }
+
             listeningDialog = AlertDialog.Builder(activity)
-                .setMessage("飞宝在呢,您请说")
+                .setView(contentView)
                 .setCancelable(false)
                 .create()
-                .also { it.show() }
+                .also { dialog ->
+                    dialog.show()
+                    // 让 Dialog 窗口本身透明,只显示自定义 view 的背景
+                    dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
+                }
         }
     }
 
@@ -162,17 +187,21 @@ object AsrHelper : OfflineCmdListener {
                 asr?.stopAsrWithMic()
                 Log.d(TAG, "ASR final result: $text")
                 dismissListeningDialog()
-                IntentRecognizeHelper.recognize(
-                    text = text,
-                    scence = scene,
-                    onSuccess = { action ->
-                        if (action.name == "goToDecisionCenter") {
-                            onGoToDecisionCenter?.invoke(action)
-                        } else {
-                            "需要跳转任务列表".showMessage()
+                if (scene == "decision") {
+                    onDirectChat?.invoke(text)
+                } else {
+                    IntentRecognizeHelper.recognize(
+                        text = text,
+                        scence = scene,
+                        onSuccess = { action ->
+                            if (action.name == "goToDecisionCenter") {
+                                onGoToDecisionCenter?.invoke(action)
+                            } else {
+                                "需要跳转任务列表".showMessage()
+                            }
                         }
-                    }
-                )
+                    )
+                }
             }
 
             override fun onFinished(taskId: String) {

+ 16 - 0
app/src/main/java/com/nova/brain/glass/model/data/TopicData.kt

@@ -0,0 +1,16 @@
+package com.nova.brain.glass.model.data
+
+//{"question":"我的任务哪个更紧急","topicId":14478,"knowledgeBaseId":"AI_ASSISTANT_MODE_FOR_GLASSES","model":"AI助手模式"}
+data class TopicData(
+    val topicName: String,
+    val knowledgeBaseId: String = "AI_ASSISTANT_MODE_FOR_GLASSES",
+)
+// "date":"2026-04-16T15:48:13.550Z",
+//"msg":"success",
+//"code":0,
+//"data": 31013,
+//"status":200
+data class TopicModel(
+    val code: Int,
+    val data: Int
+)

+ 5 - 0
app/src/main/java/com/nova/brain/glass/repository/Service.kt

@@ -3,6 +3,8 @@ package com.nova.brain.glass.repository
 import com.nova.brain.glass.model.RecognizeModel
 import com.nova.brain.glass.model.data.ChatData
 import com.nova.brain.glass.model.data.RecognizeData
+import com.nova.brain.glass.model.data.TopicData
+import com.nova.brain.glass.model.data.TopicModel
 import io.reactivex.Observable
 import okhttp3.RequestBody
 import okhttp3.ResponseBody
@@ -26,4 +28,7 @@ interface Service {
     @POST("/cbrain-gateway/cbraindep/docqa/chat/qa03")
     fun chat(@Body body: ChatData): Observable<ResponseBody>
 
+    @POST("/cbrain-gateway/cbraindep/docqa/tbtopic/save")
+    fun tbtopic(@Body body: TopicData): Observable<TopicModel>
+
 }

+ 6 - 2
app/src/main/java/com/nova/brain/glass/ui/ChatActivity.kt

@@ -51,7 +51,7 @@ class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, Activity
 
         val question = intent.getStringExtra("question") ?: ""
         if (question.isNotEmpty()) {
-            viewModel.demoPostSse(question)
+            viewModel.prepareTopic(question)
         }
     }
 
@@ -106,7 +106,10 @@ class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, Activity
         OfflineCmdServiceHelper.addOnLineListener(listener)
         AsrHelper.scene = "decision"
         AsrHelper.onGoToDecisionCenter = { action ->
-            viewModel.demoPostSse(action.params.question)
+            viewModel.prepareTopic(action.params.question)
+        }
+        AsrHelper.onDirectChat = { text ->
+            viewModel.prepareTopic(text)
         }
     }
 
@@ -114,6 +117,7 @@ class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, Activity
         super.onPause()
         OfflineCmdServiceHelper.removeOnLineListener(listener)
         AsrHelper.onGoToDecisionCenter = null
+        AsrHelper.onDirectChat = null
     }
 
     override fun onDestroy() {

+ 1 - 1
app/src/main/java/com/nova/brain/glass/ui/WelcomeActivity.kt

@@ -22,7 +22,7 @@ class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>() {
     private lateinit var vm: WelcomeVM
 
     // 点动画
-    private val tvOriginalText = "您可以说:Nova,我的任务有哪些?"
+    private val tvOriginalText = "您可以说:飞宝飞宝,我的任务有哪些?"
     private val dotsHandler = Handler(Looper.getMainLooper())
     private var dotCount = 0
     private val dotsRunnable = object : Runnable {

+ 22 - 2
app/src/main/java/com/nova/brain/glass/viewmodel/ChatVM.kt

@@ -8,12 +8,14 @@ import com.nova.brain.glass.model.ChatModel
 import com.nova.brain.glass.model.ChatModel1
 import com.nova.brain.glass.model.ChatModel2
 import com.nova.brain.glass.model.data.ChatData
+import com.nova.brain.glass.model.data.TopicData
 import com.nova.brain.glass.repository.Service
 import com.xuqm.base.common.GsonImplHelp
 import com.xuqm.base.common.LogHelper
 import com.xuqm.base.di.manager.HttpManager
 import com.xuqm.base.viewmodel.BaseListViewModel
 import com.xuqm.base.viewmodel.callback.Response
+import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
 import io.reactivex.schedulers.Schedulers
 import org.json.JSONObject
@@ -24,6 +26,7 @@ class ChatVM : BaseListViewModel<ChatItem>() {
 
     companion object {
         const val SPACER_ID = -1
+        private const val DEFAULT_TOPIC_ID = 14478
     }
 
     val result = MutableLiveData<String>()
@@ -74,7 +77,24 @@ class ChatVM : BaseListViewModel<ChatItem>() {
         }
     }
 
-    fun demoPostSse(question: String) {
+    /**
+     * 先请求 tbtopic 接口拿 topicId,成功则使用返回值,失败/code≠0 则用默认值,再发起 SSE。
+     * 进入 Chat 界面时用此方法替代直接调 demoPostSse。
+     */
+    fun prepareTopic(question: String) {
+        HttpManager.getApi(Service::class.java)
+            .tbtopic(TopicData(topicName = question))
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe({ model ->
+                val topicId = if (model.code == 0) model.data else DEFAULT_TOPIC_ID
+                demoPostSse(question, topicId)
+            }, { _ ->
+                demoPostSse(question, DEFAULT_TOPIC_ID)
+            }).also { add(it) }
+    }
+
+    fun demoPostSse(question: String, topicId: Int = DEFAULT_TOPIC_ID) {
         currentTask?.dispose()
         currentTask = null
         stopThinkingAnimation()
@@ -86,7 +106,7 @@ class ChatVM : BaseListViewModel<ChatItem>() {
         result.postValue(UUID.randomUUID().toString())
         loading.postValue(true)
 
-        currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question))
+        currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question, topicId))
             .subscribeOn(Schedulers.io())
             .subscribe({ body ->
                 var content = ""

+ 1 - 1
app/src/main/res/layout/activity_welcome.xml

@@ -11,7 +11,7 @@
             android:layout_height="wrap_content"
             android:textColor="#ff3FFF5F"
             android:textSize="19sp"
-            android:text="您可以说:Nova,我的任务有哪些?"
+            android:text="您可以说:飞宝飞宝,我的任务有哪些?"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             android:layout_marginBottom="100dp"

+ 11 - 0
app/src/main/res/layout/dialog_listening.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/tv_message"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:paddingHorizontal="32dp"
+    android:background="@color/app_color_black"
+    android:paddingVertical="20dp"
+    android:text="飞宝在呢,您请说"
+    android:textColor="#FF40FF5E"
+    android:textSize="16sp" />