diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..34c2aab --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,170 @@ +# C大脑智能眼镜应用 + +Rokid Glass 3 AR 眼镜的工业质检辅助应用。支持语音唤醒、在线 ASR/TTS、OCR 识别、AI 对话等功能,用于喷涂质检和单证检验两类业务场景。 + +--- + +## 项目结构 + +``` +c-brain-glass/ +├── app/ 主应用模块(UI、ViewModel、业务逻辑) +├── base/ 网络框架、UI 基类、工具类(Java) +├── core/ AR 相关扩展、自定义 View(Kotlin) +└── server/ 服务端相关代码(不参与 app 构建) +``` + +### app 模块包结构 + +``` +com.nova.brain.glass +├── ui/ 14 个 Activity(页面) +├── viewmodel/ 15 个 ViewModel +├── repository/ Retrofit 接口(Service.kt、Service3.kt)+ 请求拦截器 +├── helper/ 工具单例(ASR、离线关键词、媒体服务、位图解码等) +├── model/ 数据模型(请求体、响应体) +├── common/ SharedPreferences 配置、崩溃处理器 +└── MyApplication.java 全局初始化入口 +``` + +--- + +## 构建与运行 + +```bash +# 调试包 +./gradlew :app:assembleDebug + +# 正式包 +./gradlew :app:assembleRelease +``` + +- **最低 SDK**: 26(Android 8.0) +- **目标 SDK**: 29(Android 10) +- **编译 SDK**: 33 +- **Kotlin**: 2.2.0 / **Java**: 11 + +### 语音服务配置 + +语音相关密钥在 `app/build.gradle` 的 `buildConfigField` 中维护: + +| 字段 | 说明 | +|------|------| +| `SPEECH_DOMAIN` | ASR/TTS 服务器地址(IP:端口) | +| `SPEECH_AK` / `SPEECH_SK` | 鉴权 AccessKey / SecretKey | +| `SPEECH_ASR_PATH` | ASR WebSocket 路径 | +| `SPEECH_TTS_PATH` | TTS WebSocket 路径 | + +--- + +## 关键模块说明 + +### 语音交互流程 + +``` +离线唤醒词(飞宝飞宝 / C大脑) + └─ AsrHelper.onOfflineCmd() + ├─ 无网络 → showNoNetworkDialog() + └─ 有网络 → TTS 播报"在呢,您请说" + └─ TtsClient.onFinished() + └─ asrStartMic() 开始麦克风采集 + └─ AsrClient.onFinalResult() + ├─ scene=="decision" → onDirectChat(ChatActivity AI对话) + └─ 其他场景 → IntentRecognizeHelper.recognize() 意图识别 + └─ 页面导航回调 +``` + +**相关文件**: +- `helper/AsrHelper.kt` — ASR/TTS 全局管理,包含连接状态机和指数退避重连 +- `helper/OfflineCmdServiceHelper.kt` — 离线关键词注册/注销,按页面场景切换 +- `helper/IntentRecognizeHelper.kt` — 调用后台接口将语音文本映射为导航动作 + +### 场景路由(AsrHelper.scene) + +| scene 值 | 设置页面 | ASR 结果处理 | +|----------|---------|------------| +| `"home"` | WelcomeActivity | 意图识别,导航到任务中心或决策中心 | +| `"list"` | TaskListActivity | 意图识别,支持打开任务详情 | +| `"decision"` | ChatActivity | 跳过意图识别,直接发送给 AI 对话 | + +### 多 AppComponent 网络架构 + +`MyApplication` 为不同服务域名各建一套 OkHttpClient + Retrofit: + +| 字段 | 域名 | 用途 | +|------|------|------| +| `appComponent` | `baseUrl` | 主后台(任务、审阅、决策中心) | +| `appComponent1` | vicp.fun | 意图识别接口 | +| `appComponent2` | vicp.fun | 喷涂质检接口 | +| `appComponent3` | vicp.fun | 单证检验接口 | + +通过 `HttpManager.getApi(component, Service::class.java)` 获取对应实例。 + +### 位图解码 + +`helper/BitmapDecodeHelper.kt` 采用两步采样:先 `inJustDecodeBounds=true` 只读尺寸,再按 ImageView 实际大小计算 `inSampleSize`,使用 `RGB_565` 格式(比 ARGB_8888 省 50% 内存)。ImageView 解码任务通过 Activity 内部的 `imageDecodeExecutor`(单线程池)执行,在 `onDestroy()` 中 `shutdown()`。 + +--- + +## 业务流程 + +### 喷涂质检 + +``` +SprayingActivity → 获取任务信息 + └─ SprayingOCRActivity → 拍照上传,服务端 OCR 识别 + └─ SprayingResultActivity → 展示 OCR 结果(两码比对) + ├─ 通过/不合格 → SprayingFinishActivity → 确认提交 → 返回任务列表 + └─ 人工更正 → SprayingManualResultActivity → 手动选择结果 +``` + +### 单证检验 + +``` +InspectionActivity → 拍照上传,服务端识别单证 + └─ InspectionResultActivity → 展示检验结果 + ├─ 合格/不合格 → InspectionCompleteActivity + └─ 缺失单证 → InspectionMissingActivity → 补充上传 +``` + +--- + +## Activity 与离线关键词管理 + +每个 Activity 在 `onResume` 注册专属关键词,在 `onPause` 注销,避免后台页面响应语音: + +```kotlin +override fun onResume() { + OfflineCmdServiceHelper.addListenerXxx() // 注册本页关键词 + OfflineCmdServiceHelper.addOnLineListener(listener) +} +override fun onPause() { + OfflineCmdServiceHelper.removeListenerXxx() + OfflineCmdServiceHelper.removeOnLineListener(listener) +} +``` + +通用关键词(退出、返回、继续、下一个)在 `init()` 时一次性注册,不随页面切换变化。 + +--- + +## 主要依赖 + +| 依赖 | 版本 | 用途 | +|------|------|------| +| `glass3.open.sdk` | 2.1.7-E | Rokid Glass 硬件 SDK | +| `online-speech` | 0.1.0 | Rokid 在线 ASR/TTS | +| `retrofit2` | 2.4.0 | HTTP 网络请求 | +| `rxjava2` / `rxandroid` | — | 异步流处理 | +| `dagger` | 2.40.5 | 依赖注入 | +| `glide` | 4.10.0 | 图片加载 | +| `markwon` | 4.6.2 | Markdown 渲染(AI 对话) | + +--- + +## 注意事项 + +- **Glass SDK 初始化顺序**:`OfflineCmdServiceHelper.init()` 必须在 `AsrHelper.init()` 之前,因为后者依赖前者注册唤醒词。 +- **内存**:设备内存有限,ImageView 在 `onDestroy()` 中需调用 `setImageDrawable(null)` 释放 Bitmap 引用;解码线程池在 `onDestroy()` 中 `shutdown()`。 +- **SSL**:内网服务器使用自签证书,`trustAllCerts = true`,生产环境应替换为正式证书。 +- **网络切换**:ASR/TTS 连接在网络断开后进入指数退避重连,网络恢复时立即重置并重连,无需手动干预。 \ No newline at end of file diff --git a/app/src/main/java/com/nova/brain/glass/MyApplication.java b/app/src/main/java/com/nova/brain/glass/MyApplication.java index fafacf9..c2002d1 100644 --- a/app/src/main/java/com/nova/brain/glass/MyApplication.java +++ b/app/src/main/java/com/nova/brain/glass/MyApplication.java @@ -11,22 +11,41 @@ import com.xuqm.base.di.component.AppComponent; import com.xuqm.base.di.manager.HttpManager; /** + * 应用全局入口。 + * + *
负责以下初始化工作: + *
各 AppComponent 对应关系: + *
Glass SDK 需要通过系统 AIDL 服务完成硬件鉴权,绑定成功后才能使用离线关键词和相机等能力。
+ * 离线关键词({@link OfflineCmdServiceHelper})和在线语音({@link AsrHelper})必须在此回调中初始化,
+ * 否则底层 SDK 尚未就绪,调用会静默失败。
+ */
void initSdk() {
- // 如果SDK已经初始化了,则直接返回
+ // SDK 已就绪时无需重复绑定(进程保活场景下可能触发)
if (GlassSdk.isReady()) {
return;
}
GlassSdk.bindSecurityService(Utils.getApp(), new IServiceConnectionCallback() {
@Override
public void onServiceConnected() {
+ // 先注册离线关键词,再初始化在线语音(ASR 唤醒词依赖离线服务)
OfflineCmdServiceHelper.INSTANCE.init();
AsrHelper.INSTANCE.init();
}
@Override
public void onServiceDisconnected() {
-
+ // 系统服务断开,硬件能力暂时不可用,等待系统自动重连
}
@Override
public void onBindingDied() {
-
+ // Binder 死亡(通常是系统进程崩溃),需要重新绑定;当前版本依赖系统自恢复
}
});
}
diff --git a/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt b/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt
index e3b3df2..b849d28 100644
--- a/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt
+++ b/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt
@@ -27,6 +27,25 @@ import com.rokid.online.speech.open.OpenSdkAudioSource
import com.xuqm.base.common.AppManager
import kotlin.system.exitProcess
+/**
+ * 在线语音识别(ASR)与语音合成(TTS)的全局管理单例。
+ *
+ * ## 工作流程
+ * 1. 离线关键词引擎检测到唤醒词("飞宝飞宝" / "C大脑")→ [onOfflineCmd]
+ * 2. TTS 播报唤醒响应("在呢,您请说")→ [TtsClient.Listener.onFinished]
+ * 3. 启动麦克风开始 ASR 流式识别 → [asrStartMic]
+ * 4. 识别完成后调用 [IntentRecognizeHelper.recognize] 将文本映射为页面导航动作
+ *
+ * ## 连接管理
+ * - ASR 和 TTS 各自维护独立的 WebSocket 连接,断线后采用指数退避策略自动重连
+ * (基础间隔 3s,最大间隔 30s,最多重试 5 次)
+ * - 超过重试上限后置为 paused 状态;网络恢复时([networkCallback])重置并立即重连
+ *
+ * ## 场景路由
+ * [scene] 由各 Activity 在 onResume/onPause 中设置,决定 ASR 结果的处理方式:
+ * - "home" / "list" → 意图识别后执行页面跳转
+ * - "decision" → 文本直接发送给 ChatActivity 发起 AI 对话
+ */
object AsrHelper : OfflineCmdListener {
private const val TAG = "AsrHelper"
@@ -40,38 +59,45 @@ object AsrHelper : OfflineCmdListener {
private val ASR_PATH get() = BuildConfig.SPEECH_ASR_PATH
private val TTS_PATH get() = BuildConfig.SPEECH_TTS_PATH
- // 唤醒词:Nova Nova
+ // 支持的唤醒词(与 OfflineCmdServiceHelper.registerAsrWakeWord 中注册的词保持一致)
private const val WAKE_WORD1 = "飞宝飞宝"
private var sdk: OnlineSpeechSdk? = null
private var asr: AsrClient? = null
private var tts: TtsClient? = null
+ // audioSource 和 ttsPlayer 由 SDK 内部管理生命周期,随 asr/tts 连接一同关闭
private val audioSource = OpenSdkAudioSource()
private val ttsPlayer = AndroidPcmTtsStreamPlayer()
- private var isConnected = false
- private var isMicRunning = false
- private var isTtsConnected = false
- private var isAsrConnecting = false
- private var isTtsConnecting = false
- private var pendingStartMic = false
- private var isClosed = false
- private var asrReconnectAttempt = 0
- private var ttsReconnectAttempt = 0
- private var asrReconnectPaused = false
- private var ttsReconnectPaused = false
+ // ---- 连接状态机 ----
+ private var isConnected = false // ASR WebSocket 已连接
+ private var isMicRunning = false // 麦克风正在采集音频
+ private var isTtsConnected = false // TTS WebSocket 已连接
+ private var isAsrConnecting = false // ASR 正在建立连接(防止重复发起)
+ private var isTtsConnecting = false // TTS 正在建立连接(防止重复发起)
+ private var pendingStartMic = false // ASR 未连接时收到启动麦克风请求,连接成功后补发
+ private var isClosed = false // 已调用 close(),阻止重连循环
+ private var asrReconnectAttempt = 0 // ASR 当前重连次数
+ private var ttsReconnectAttempt = 0 // TTS 当前重连次数
+ private var asrReconnectPaused = false // ASR 重连已达上限,暂停直到网络恢复
+ private var ttsReconnectPaused = false // TTS 重连已达上限,暂停直到网络恢复
private var connectivityManager: ConnectivityManager? = null
private var networkCallbackRegistered = false
+ // 唤醒响应文本(TTS 播报,播报结束后自动开启麦克风)
private const val WAKE_RESPONSE = "在呢,您请说"
+ // 用户说话超时:30 秒内无最终结果则自动停止麦克风
private const val LISTENING_TIMEOUT_MS = 30_000L
+ // 重连退避参数
private const val BASE_RECONNECT_DELAY_MS = 3_000L
private const val MAX_RECONNECT_DELAY_MS = 30_000L
private const val MAX_RECONNECT_ATTEMPTS = 5
private val mainHandler = Handler(Looper.getMainLooper())
- private var listeningDialog: AlertDialog? = null
- private var noNetworkDialog: AlertDialog? = null
+ private var listeningDialog: AlertDialog? = null // 正在聆听的半透明提示框
+ private var noNetworkDialog: AlertDialog? = null // 无网络时的全屏提示框
+
+ // 30 秒内无最终结果时自动停止麦克风,防止长时间占用音频焦点
private val listeningTimeoutRunnable = Runnable {
if (!isMicRunning) return@Runnable
runCatching { asr?.stopAsrWithMic() }
@@ -79,6 +105,8 @@ object AsrHelper : OfflineCmdListener {
dismissListeningDialog()
Log.d(TAG, "ASR listening timeout after ${LISTENING_TIMEOUT_MS}ms")
}
+
+ // 每秒检查一次网络;恢复后关闭无网对话框,否则持续显示
private val networkCheckRunnable = object : Runnable {
override fun run() {
if (isNetworkAvailable()) {
@@ -89,6 +117,8 @@ object AsrHelper : OfflineCmdListener {
mainHandler.postDelayed(this, 1000L)
}
}
+
+ // 延迟重连任务(由 scheduleAsrReconnect 投递到 mainHandler)
private val asrReconnectRunnable = Runnable {
if (shouldReconnectAsr()) {
asrConnect()
@@ -99,6 +129,8 @@ object AsrHelper : OfflineCmdListener {
ttsConnect()
}
}
+
+ // 网络状态监听:恢复时立即重连,不需要等待下一轮轮询
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Log.d(TAG, "Network available, reconnect speech channels if needed")
@@ -112,27 +144,34 @@ object AsrHelper : OfflineCmdListener {
}
}
- // 拼接每次识别会话中的中间结果
+ // 用 StringBuilder 累积本次会话的中间识别结果,避免频繁创建短生命周期 String 对象
private val currentPartial = StringBuilder()
- /** 当前页面的场景标识,由各 Activity 在 onResume/onPause 中维护 */
+ /**
+ * 当前页面的场景标识,由各 Activity 在 onResume/onPause 中维护。
+ * 可选值:"home" | "list" | "decision"
+ */
var scene: String = "home"
- /** goToDecisionCenter 命中时的回调,由各 Activity 在 onResume/onPause 中注册/清空 */
+ /** goToDecisionCenter 意图命中时的回调,由各 Activity 在 onResume/onPause 中注册/清空 */
var onGoToDecisionCenter: ((action: RecognizeAction) -> Unit)? = null
- /** goToTaskCenter 命中时的回调,由各 Activity 在 onResume/onPause 中注册/清空 */
+ /** goToTaskCenter 意图命中时的回调,由各 Activity 在 onResume/onPause 中注册/清空 */
var onGoToTaskCenter: ((action: RecognizeAction) -> Unit)? = null
- /** list 场景下由 TaskListActivity 注册,返回当前列表数据作为 extra 传给服务端 */
+ /** list 场景下由 TaskListActivity 注册,返回当前列表数据作为 extra 传给服务端辅助意图识别 */
var extraProvider: (() -> List