|
@@ -1,6 +1,5 @@
|
|
|
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.Color
|
|
|
import android.graphics.drawable.GradientDrawable
|
|
import android.graphics.drawable.GradientDrawable
|
|
@@ -36,7 +35,6 @@ object AsrHelper : OfflineCmdListener {
|
|
|
// 唤醒词:Nova Nova
|
|
// 唤醒词:Nova Nova
|
|
|
private const val WAKE_WORD = "Nova Nova"
|
|
private const val WAKE_WORD = "Nova Nova"
|
|
|
private const val WAKE_WORD1 = "飞宝飞宝"
|
|
private const val WAKE_WORD1 = "飞宝飞宝"
|
|
|
- private const val WAKE_WORD_PINYIN = "nou wa nou wa"
|
|
|
|
|
|
|
|
|
|
private var sdk: OnlineSpeechSdk? = null
|
|
private var sdk: OnlineSpeechSdk? = null
|
|
|
private var asr: AsrClient? = null
|
|
private var asr: AsrClient? = null
|
|
@@ -48,10 +46,22 @@ object AsrHelper : OfflineCmdListener {
|
|
|
private var isMicRunning = false
|
|
private var isMicRunning = false
|
|
|
private var isTtsConnected = false
|
|
private var isTtsConnected = false
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * true:唤醒词已触发、TTS 播完,下一条 ASR 结果会被处理。
|
|
|
|
|
+ * false:静默监听阶段,识别到语音也不处理。
|
|
|
|
|
+ */
|
|
|
|
|
+ private var waitingForCommand = false
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * TTS 正在播放期间置 true,禁止 ASR 重启麦克风(避免录入 TTS 音频)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private var ttsPlaying = false
|
|
|
|
|
+
|
|
|
private const val WAKE_RESPONSE = "在呢,您请说"
|
|
private const val WAKE_RESPONSE = "在呢,您请说"
|
|
|
|
|
|
|
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
|
private var listeningDialog: AlertDialog? = null
|
|
private var listeningDialog: AlertDialog? = null
|
|
|
|
|
+ private var listeningDialogTv: android.widget.TextView? = null
|
|
|
|
|
|
|
|
// 拼接每次识别会话中的中间结果
|
|
// 拼接每次识别会话中的中间结果
|
|
|
private var currentPartial = ""
|
|
private var currentPartial = ""
|
|
@@ -89,11 +99,9 @@ object AsrHelper : OfflineCmdListener {
|
|
|
asr = sdk!!.createAsrClient().attachAudioSource(audioSource).also { setupAsrCallbacks(it) }
|
|
asr = sdk!!.createAsrClient().attachAudioSource(audioSource).also { setupAsrCallbacks(it) }
|
|
|
tts = sdk!!.createTtsClient().attachStreamPlayer(ttsPlayer).also { setupTtsCallbacks(it) }
|
|
tts = sdk!!.createTtsClient().attachStreamPlayer(ttsPlayer).also { setupTtsCallbacks(it) }
|
|
|
|
|
|
|
|
- // 注册离线关键词 Nova Nova,GlassSdk 触发后启动 ASR
|
|
|
|
|
OfflineCmdServiceHelper.registerAsrWakeWord()
|
|
OfflineCmdServiceHelper.registerAsrWakeWord()
|
|
|
OfflineCmdServiceHelper.addOnLineListener(this)
|
|
OfflineCmdServiceHelper.addOnLineListener(this)
|
|
|
|
|
|
|
|
- // 自动建立 ASR / TTS 连接
|
|
|
|
|
asrConnect()
|
|
asrConnect()
|
|
|
tts?.connect()
|
|
tts?.connect()
|
|
|
|
|
|
|
@@ -105,7 +113,11 @@ object AsrHelper : OfflineCmdListener {
|
|
|
Log.d(TAG, "ASR connect() called")
|
|
Log.d(TAG, "ASR connect() called")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun asrStartMic() {
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @param showDialog true 时表示"等待用户指令"阶段,弹出提示框;
|
|
|
|
|
+ * false 时为静默持续监听,不打扰用户。
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun asrStartMic(showDialog: Boolean = false) {
|
|
|
if (!isConnected) {
|
|
if (!isConnected) {
|
|
|
Log.w(TAG, "ASR startMic ignored: not connected")
|
|
Log.w(TAG, "ASR startMic ignored: not connected")
|
|
|
return
|
|
return
|
|
@@ -117,22 +129,31 @@ object AsrHelper : OfflineCmdListener {
|
|
|
runCatching { asr?.startAsrWithMic() }
|
|
runCatching { asr?.startAsrWithMic() }
|
|
|
.onSuccess {
|
|
.onSuccess {
|
|
|
isMicRunning = true
|
|
isMicRunning = true
|
|
|
- Log.d(TAG, "ASR startAsrWithMic()")
|
|
|
|
|
|
|
+ Log.d(TAG, "ASR startAsrWithMic() showDialog=$showDialog")
|
|
|
|
|
+ if (showDialog) showListeningDialog()
|
|
|
}
|
|
}
|
|
|
.onFailure { Log.e(TAG, "ASR startAsrWithMic failed: ${it.message}") }
|
|
.onFailure { Log.e(TAG, "ASR startAsrWithMic failed: ${it.message}") }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private fun stopMicIfRunning() {
|
|
|
|
|
+ if (!isMicRunning) return
|
|
|
|
|
+ runCatching { asr?.stopAsrWithMic() }
|
|
|
|
|
+ isMicRunning = false
|
|
|
|
|
+ Log.d(TAG, "ASR mic stopped")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private fun showListeningDialog() {
|
|
private fun showListeningDialog() {
|
|
|
mainHandler.post {
|
|
mainHandler.post {
|
|
|
listeningDialog?.dismiss()
|
|
listeningDialog?.dismiss()
|
|
|
|
|
+ listeningDialogTv = null
|
|
|
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)
|
|
val contentView = LayoutInflater.from(activity)
|
|
|
.inflate(R.layout.dialog_listening, null)
|
|
.inflate(R.layout.dialog_listening, null)
|
|
|
|
|
+ listeningDialogTv = contentView.findViewById(R.id.tv_message)
|
|
|
|
|
|
|
|
- // 黑底 + 绿色圆角线框
|
|
|
|
|
val density = activity.resources.displayMetrics.density
|
|
val density = activity.resources.displayMetrics.density
|
|
|
contentView.background = GradientDrawable().apply {
|
|
contentView.background = GradientDrawable().apply {
|
|
|
shape = GradientDrawable.RECTANGLE
|
|
shape = GradientDrawable.RECTANGLE
|
|
@@ -147,16 +168,22 @@ object AsrHelper : OfflineCmdListener {
|
|
|
.create()
|
|
.create()
|
|
|
.also { dialog ->
|
|
.also { dialog ->
|
|
|
dialog.show()
|
|
dialog.show()
|
|
|
- // 让 Dialog 窗口本身透明,只显示自定义 view 的背景
|
|
|
|
|
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
|
|
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private fun updateListeningDialogText(text: String) {
|
|
|
|
|
+ mainHandler.post {
|
|
|
|
|
+ listeningDialogTv?.text = text
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private fun dismissListeningDialog() {
|
|
private fun dismissListeningDialog() {
|
|
|
mainHandler.post {
|
|
mainHandler.post {
|
|
|
listeningDialog?.dismiss()
|
|
listeningDialog?.dismiss()
|
|
|
listeningDialog = null
|
|
listeningDialog = null
|
|
|
|
|
+ listeningDialogTv = null
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -164,53 +191,77 @@ object AsrHelper : OfflineCmdListener {
|
|
|
asrClient.setListener(object : AsrClient.Listener {
|
|
asrClient.setListener(object : AsrClient.Listener {
|
|
|
override fun onOpen() {
|
|
override fun onOpen() {
|
|
|
isConnected = true
|
|
isConnected = true
|
|
|
- Log.d(TAG, "ASR websocket open")
|
|
|
|
|
|
|
+ Log.d(TAG, "ASR websocket open, starting continuous listening")
|
|
|
|
|
+ // ASR 连接成功后立即开启持续监听
|
|
|
|
|
+ asrStartMic(showDialog = false)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onStart(taskId: String) {
|
|
override fun onStart(taskId: String) {
|
|
|
currentPartial = ""
|
|
currentPartial = ""
|
|
|
Log.d(TAG, "ASR started: $taskId")
|
|
Log.d(TAG, "ASR started: $taskId")
|
|
|
|
|
+ if (waitingForCommand) {
|
|
|
|
|
+ updateListeningDialogText("识别中...")
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onPartialResult(taskId: String, text: String) {
|
|
override fun onPartialResult(taskId: String, text: String) {
|
|
|
- // 滚动更新当前识别中间结果
|
|
|
|
|
currentPartial += text
|
|
currentPartial += text
|
|
|
Log.d(TAG, "ASR partial: $text")
|
|
Log.d(TAG, "ASR partial: $text")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onFinalResult(taskId: String, text: String) {
|
|
override fun onFinalResult(taskId: String, text: String) {
|
|
|
- // 将最终结果追加拼接到会话字符串
|
|
|
|
|
isMicRunning = false
|
|
isMicRunning = false
|
|
|
- // 滚动更新当前识别中间结果
|
|
|
|
|
currentPartial += text
|
|
currentPartial += text
|
|
|
- asr?.stopAsrWithMic()
|
|
|
|
|
- Log.d(TAG, "ASR final result: $text")
|
|
|
|
|
- dismissListeningDialog()
|
|
|
|
|
- if (scene == "decision") {
|
|
|
|
|
- onDirectChat?.invoke(text)
|
|
|
|
|
|
|
+ Log.d(TAG, "ASR final result: [$text] waitingForCommand=$waitingForCommand")
|
|
|
|
|
+
|
|
|
|
|
+ if (waitingForCommand) {
|
|
|
|
|
+ // ── 情况①:正在等待用户说出指令 ──
|
|
|
|
|
+ waitingForCommand = false
|
|
|
|
|
+ dismissListeningDialog()
|
|
|
|
|
+ processCommandText(text)
|
|
|
} else {
|
|
} else {
|
|
|
- IntentRecognizeHelper.recognize(
|
|
|
|
|
- text = text,
|
|
|
|
|
- scence = scene,
|
|
|
|
|
- onSuccess = { action ->
|
|
|
|
|
- if (action.name == "goToDecisionCenter") {
|
|
|
|
|
- onGoToDecisionCenter?.invoke(action)
|
|
|
|
|
- } else {
|
|
|
|
|
- "需要跳转任务列表".showMessage()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // ── 静默监听阶段 ──
|
|
|
|
|
+ val wakeWord = findWakeWordIn(text)
|
|
|
|
|
+ if (wakeWord != null) {
|
|
|
|
|
+ // 提取唤醒词后面的内容(去掉标点前缀)
|
|
|
|
|
+ val content = text.substringAfter(wakeWord)
|
|
|
|
|
+ .trimStart(',', ',', '、', ' ', '。', '.')
|
|
|
|
|
+ if (content.isNotBlank()) {
|
|
|
|
|
+ // ── 情况②:唤醒词 + 内容 → 直接处理,跳过 TTS 确认 ──
|
|
|
|
|
+ Log.d(TAG, "Wake word + content, process directly: [$content]")
|
|
|
|
|
+ processCommandText(content)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // ── 情况③:仅唤醒词 → 走 TTS 确认 + 等待下一句 ──
|
|
|
|
|
+ Log.d(TAG, "Wake word only, entering wait-for-command mode")
|
|
|
|
|
+ triggerWakeFlow()
|
|
|
}
|
|
}
|
|
|
- )
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // ── 情况④:普通语音 → 关键词匹配(导航命令等)──
|
|
|
|
|
+ OfflineCmdServiceHelper.matchAndDispatch(text)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ // onFinished 会负责重启麦克风
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onFinished(taskId: String) {
|
|
override fun onFinished(taskId: String) {
|
|
|
Log.d(TAG, "ASR ended: $taskId")
|
|
Log.d(TAG, "ASR ended: $taskId")
|
|
|
|
|
+ // TTS 未在播放时自动重启,保持持续监听
|
|
|
|
|
+ if (!ttsPlaying) {
|
|
|
|
|
+ asrStartMic(showDialog = false)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onError(code: Int, message: String) {
|
|
override fun onError(code: Int, message: String) {
|
|
|
Log.e(TAG, "ASR error code=$code msg=$message")
|
|
Log.e(TAG, "ASR error code=$code msg=$message")
|
|
|
isMicRunning = false
|
|
isMicRunning = false
|
|
|
- dismissListeningDialog()
|
|
|
|
|
|
|
+ if (waitingForCommand) {
|
|
|
|
|
+ waitingForCommand = false
|
|
|
|
|
+ dismissListeningDialog()
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!ttsPlaying) {
|
|
|
|
|
+ // 遇到错误延迟 1s 重试
|
|
|
|
|
+ mainHandler.postDelayed({ asrStartMic(showDialog = false) }, 1000)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onClosed(code: Int, reason: String) {
|
|
override fun onClosed(code: Int, reason: String) {
|
|
@@ -230,13 +281,18 @@ object AsrHelper : OfflineCmdListener {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onFinished(taskId: String) {
|
|
override fun onFinished(taskId: String) {
|
|
|
- Log.d(TAG, "TTS ended: $taskId, starting mic")
|
|
|
|
|
- asrStartMic()
|
|
|
|
|
|
|
+ Log.d(TAG, "TTS ended: $taskId")
|
|
|
|
|
+ ttsPlaying = false
|
|
|
|
|
+ // TTS 播完:进入"等待指令"模式,开麦并弹出提示
|
|
|
|
|
+ waitingForCommand = true
|
|
|
|
|
+ asrStartMic(showDialog = true)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onError(code: Int, message: String) {
|
|
override fun onError(code: Int, message: String) {
|
|
|
- Log.e(TAG, "TTS error code=$code msg=$message, fallback to mic")
|
|
|
|
|
- asrStartMic()
|
|
|
|
|
|
|
+ Log.e(TAG, "TTS error code=$code msg=$message, fallback")
|
|
|
|
|
+ ttsPlaying = false
|
|
|
|
|
+ waitingForCommand = true
|
|
|
|
|
+ asrStartMic(showDialog = true)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onClosed(code: Int, reason: String) {
|
|
override fun onClosed(code: Int, reason: String) {
|
|
@@ -246,27 +302,66 @@ object AsrHelper : OfflineCmdListener {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 离线关键词回调:唤醒词触发时先 TTS 播报,播报结束后启动麦克风
|
|
|
|
|
|
|
+ /** 是否是唤醒词 */
|
|
|
|
|
+ private fun isWakeWord(cmd: String) = cmd == WAKE_WORD || cmd == WAKE_WORD1 || cmd == "C大脑"
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从文本中找到第一个唤醒词,返回该唤醒词;未找到返回 null。
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun findWakeWordIn(text: String): String? =
|
|
|
|
|
+ listOf(WAKE_WORD, WAKE_WORD1, "C大脑").firstOrNull { text.contains(it) }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 触发唤醒流程:停麦 → TTS 播报 → TTS 结束后进入等待指令模式。
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun triggerWakeFlow() {
|
|
|
|
|
+ stopMicIfRunning()
|
|
|
|
|
+ waitingForCommand = false
|
|
|
|
|
+ if (isTtsConnected) {
|
|
|
|
|
+ ttsPlaying = true
|
|
|
|
|
+ tts?.speak(WAKE_RESPONSE)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Log.w(TAG, "TTS not connected, entering command mode directly")
|
|
|
|
|
+ waitingForCommand = true
|
|
|
|
|
+ asrStartMic(showDialog = true)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理已确认的指令文本(scene 已就绪时调用)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun processCommandText(text: String) {
|
|
|
|
|
+ if (scene == "decision") {
|
|
|
|
|
+ onDirectChat?.invoke(text)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ IntentRecognizeHelper.recognize(
|
|
|
|
|
+ text = text,
|
|
|
|
|
+ scence = scene,
|
|
|
|
|
+ onSuccess = { action ->
|
|
|
|
|
+ if (action.name == "goToDecisionCenter") {
|
|
|
|
|
+ onGoToDecisionCenter?.invoke(action)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ "需要跳转任务列表".showMessage()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 离线关键词回调(现在仅处理唤醒词;其他关键词由 matchAndDispatch 直接派发给各 Activity)
|
|
|
override fun onOfflineCmd(cmd: String) {
|
|
override fun onOfflineCmd(cmd: String) {
|
|
|
- if (cmd == WAKE_WORD || cmd == WAKE_WORD1 || cmd == "C大脑") {
|
|
|
|
|
- Log.d(TAG, "Wake word triggered")
|
|
|
|
|
- showListeningDialog()
|
|
|
|
|
- if (isTtsConnected) {
|
|
|
|
|
- tts?.speak(WAKE_RESPONSE)
|
|
|
|
|
- } else {
|
|
|
|
|
- Log.w(TAG, "TTS not connected, starting mic directly")
|
|
|
|
|
- asrStartMic()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (isWakeWord(cmd)) {
|
|
|
|
|
+ Log.d(TAG, "Wake word triggered via onOfflineCmd")
|
|
|
|
|
+ triggerWakeFlow()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun close() {
|
|
fun close() {
|
|
|
OfflineCmdServiceHelper.removeOnLineListener(this)
|
|
OfflineCmdServiceHelper.removeOnLineListener(this)
|
|
|
- if (isMicRunning) {
|
|
|
|
|
- runCatching { asr?.stopAsrWithMic() }
|
|
|
|
|
- isMicRunning = false
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ stopMicIfRunning()
|
|
|
dismissListeningDialog()
|
|
dismissListeningDialog()
|
|
|
|
|
+ waitingForCommand = false
|
|
|
|
|
+ ttsPlaying = false
|
|
|
asr?.close()
|
|
asr?.close()
|
|
|
tts?.close()
|
|
tts?.close()
|
|
|
sdk?.close()
|
|
sdk?.close()
|
|
@@ -277,4 +372,4 @@ object AsrHelper : OfflineCmdListener {
|
|
|
isTtsConnected = false
|
|
isTtsConnected = false
|
|
|
Log.d(TAG, "AsrHelper closed")
|
|
Log.d(TAG, "AsrHelper closed")
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+}
|