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

- 添加了自定义语音识别对话框界面,支持黑色背景和绿色圆角边框
- 实现了场景切换功能,decision场景下可直接进行聊天而无需意图识别
- 新增TopicData数据模型用于话题管理
- 集成tbtopic接口实现话题ID获取功能
- 更新ChatActivity在进入时预创建话题
- 修正欢迎界面唤醒词提示文本为"飞宝飞宝"
- 重构ASR回调处理逻辑,添加onDirectChat回调支持
这个提交包含在:
徐勤民 2026-04-17 00:13:48 +08:00
父节点 2757854f53
当前提交 5f44916c90
共有 8 个文件被更改,包括 103 次插入18 次删除

查看文件

@ -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) {

查看文件

@ -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
)

查看文件

@ -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>
}

查看文件

@ -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() {

查看文件

@ -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 {

查看文件

@ -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成功则使用返回值失败/code0 则用默认值再发起 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 = ""

查看文件

@ -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"

查看文件

@ -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" />