feat(asr): 优化语音识别功能并添加话题管理支持
- 添加了自定义语音识别对话框界面,支持黑色背景和绿色圆角边框 - 实现了场景切换功能,decision场景下可直接进行聊天而无需意图识别 - 新增TopicData数据模型用于话题管理 - 集成tbtopic接口实现话题ID获取功能 - 更新ChatActivity在进入时预创建话题 - 修正欢迎界面唤醒词提示文本为"飞宝飞宝" - 重构ASR回调处理逻辑,添加onDirectChat回调支持
这个提交包含在:
父节点
2757854f53
当前提交
5f44916c90
@ -1,10 +1,15 @@
|
|||||||
package com.nova.brain.glass.helper
|
package com.nova.brain.glass.helper
|
||||||
|
|
||||||
|
import android.R.attr.action
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
import com.nova.brain.glass.BuildConfig
|
import com.nova.brain.glass.BuildConfig
|
||||||
|
import com.nova.brain.glass.R
|
||||||
import com.nova.brain.glass.model.RecognizeAction
|
import com.nova.brain.glass.model.RecognizeAction
|
||||||
import com.rokid.online.speech.AsrClient
|
import com.rokid.online.speech.AsrClient
|
||||||
import com.rokid.online.speech.OnlineSpeechSdk
|
import com.rokid.online.speech.OnlineSpeechSdk
|
||||||
@ -57,6 +62,9 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
/** goToDecisionCenter 命中时的回调,由各 Activity 在 onResume/onPause 中注册/清空 */
|
/** goToDecisionCenter 命中时的回调,由各 Activity 在 onResume/onPause 中注册/清空 */
|
||||||
var onGoToDecisionCenter: ((action: RecognizeAction) -> Unit)? = null
|
var onGoToDecisionCenter: ((action: RecognizeAction) -> Unit)? = null
|
||||||
|
|
||||||
|
/** scene == "decision" 时直接用 ASR 文本发起对话,由 ChatActivity 注册 */
|
||||||
|
var onDirectChat: ((text: String) -> Unit)? = null
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
val cfg = OnlineSpeechSdkConfig(
|
val cfg = OnlineSpeechSdkConfig(
|
||||||
domain = DOMAIN,
|
domain = DOMAIN,
|
||||||
@ -121,11 +129,28 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
val activity = runCatching { AppManager.getInstance().getActivity() }.getOrNull()
|
val activity = runCatching { AppManager.getInstance().getActivity() }.getOrNull()
|
||||||
?: return@post
|
?: return@post
|
||||||
if (activity.isFinishing || activity.isDestroyed) 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)
|
listeningDialog = AlertDialog.Builder(activity)
|
||||||
.setMessage("飞宝在呢,您请说")
|
.setView(contentView)
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.create()
|
.create()
|
||||||
.also { it.show() }
|
.also { dialog ->
|
||||||
|
dialog.show()
|
||||||
|
// 让 Dialog 窗口本身透明,只显示自定义 view 的背景
|
||||||
|
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,6 +187,9 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
asr?.stopAsrWithMic()
|
asr?.stopAsrWithMic()
|
||||||
Log.d(TAG, "ASR final result: $text")
|
Log.d(TAG, "ASR final result: $text")
|
||||||
dismissListeningDialog()
|
dismissListeningDialog()
|
||||||
|
if (scene == "decision") {
|
||||||
|
onDirectChat?.invoke(text)
|
||||||
|
} else {
|
||||||
IntentRecognizeHelper.recognize(
|
IntentRecognizeHelper.recognize(
|
||||||
text = text,
|
text = text,
|
||||||
scence = scene,
|
scence = scene,
|
||||||
@ -174,6 +202,7 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFinished(taskId: String) {
|
override fun onFinished(taskId: String) {
|
||||||
Log.d(TAG, "ASR ended: $taskId")
|
Log.d(TAG, "ASR ended: $taskId")
|
||||||
|
|||||||
@ -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.RecognizeModel
|
||||||
import com.nova.brain.glass.model.data.ChatData
|
import com.nova.brain.glass.model.data.ChatData
|
||||||
import com.nova.brain.glass.model.data.RecognizeData
|
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 io.reactivex.Observable
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
@ -26,4 +28,7 @@ interface Service {
|
|||||||
@POST("/cbrain-gateway/cbraindep/docqa/chat/qa03")
|
@POST("/cbrain-gateway/cbraindep/docqa/chat/qa03")
|
||||||
fun chat(@Body body: ChatData): Observable<ResponseBody>
|
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") ?: ""
|
val question = intent.getStringExtra("question") ?: ""
|
||||||
if (question.isNotEmpty()) {
|
if (question.isNotEmpty()) {
|
||||||
viewModel.demoPostSse(question)
|
viewModel.prepareTopic(question)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +106,10 @@ class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, Activity
|
|||||||
OfflineCmdServiceHelper.addOnLineListener(listener)
|
OfflineCmdServiceHelper.addOnLineListener(listener)
|
||||||
AsrHelper.scene = "decision"
|
AsrHelper.scene = "decision"
|
||||||
AsrHelper.onGoToDecisionCenter = { action ->
|
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()
|
super.onPause()
|
||||||
OfflineCmdServiceHelper.removeOnLineListener(listener)
|
OfflineCmdServiceHelper.removeOnLineListener(listener)
|
||||||
AsrHelper.onGoToDecisionCenter = null
|
AsrHelper.onGoToDecisionCenter = null
|
||||||
|
AsrHelper.onDirectChat = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>() {
|
|||||||
private lateinit var vm: WelcomeVM
|
private lateinit var vm: WelcomeVM
|
||||||
|
|
||||||
// 点动画
|
// 点动画
|
||||||
private val tvOriginalText = "您可以说:Nova,我的任务有哪些?"
|
private val tvOriginalText = "您可以说:飞宝飞宝,我的任务有哪些?"
|
||||||
private val dotsHandler = Handler(Looper.getMainLooper())
|
private val dotsHandler = Handler(Looper.getMainLooper())
|
||||||
private var dotCount = 0
|
private var dotCount = 0
|
||||||
private val dotsRunnable = object : Runnable {
|
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.ChatModel1
|
||||||
import com.nova.brain.glass.model.ChatModel2
|
import com.nova.brain.glass.model.ChatModel2
|
||||||
import com.nova.brain.glass.model.data.ChatData
|
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.nova.brain.glass.repository.Service
|
||||||
import com.xuqm.base.common.GsonImplHelp
|
import com.xuqm.base.common.GsonImplHelp
|
||||||
import com.xuqm.base.common.LogHelper
|
import com.xuqm.base.common.LogHelper
|
||||||
import com.xuqm.base.di.manager.HttpManager
|
import com.xuqm.base.di.manager.HttpManager
|
||||||
import com.xuqm.base.viewmodel.BaseListViewModel
|
import com.xuqm.base.viewmodel.BaseListViewModel
|
||||||
import com.xuqm.base.viewmodel.callback.Response
|
import com.xuqm.base.viewmodel.callback.Response
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
@ -24,6 +26,7 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SPACER_ID = -1
|
const val SPACER_ID = -1
|
||||||
|
private const val DEFAULT_TOPIC_ID = 14478
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = MutableLiveData<String>()
|
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?.dispose()
|
||||||
currentTask = null
|
currentTask = null
|
||||||
stopThinkingAnimation()
|
stopThinkingAnimation()
|
||||||
@ -86,7 +106,7 @@ class ChatVM : BaseListViewModel<ChatItem>() {
|
|||||||
result.postValue(UUID.randomUUID().toString())
|
result.postValue(UUID.randomUUID().toString())
|
||||||
loading.postValue(true)
|
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())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({ body ->
|
.subscribe({ body ->
|
||||||
var content = ""
|
var content = ""
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="#ff3FFF5F"
|
android:textColor="#ff3FFF5F"
|
||||||
android:textSize="19sp"
|
android:textSize="19sp"
|
||||||
android:text="您可以说:Nova,我的任务有哪些?"
|
android:text="您可以说:飞宝飞宝,我的任务有哪些?"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:layout_marginBottom="100dp"
|
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" />
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户