智眸:基于 Rokid Glasses的房产经纪人带看辅助应用
Slogan:镜在眼前,智慧搭伴
一、应用介绍
1.1 经纪人工作场景与痛点
房产经纪人的日常工作核心是"带看"——带领客户实地查看房源。在这个过程中,经纪人需要:
- 实时介绍小区信息:建筑年代、容积率、绿化率、物业费、燃气费等核心数据
- 解答客户疑问:周边配套、学区政策、交通状况等
- 记录客户需求:客户反馈、关注点、意向程度等
传统作业模式下,经纪人面临以下痛点 (尤其是新手经纪人和跨区带看经纪人):
- 信息查阅不便:需要掏出手机查看信息,打断带看节奏
- 专业形象受损:低头看手机给客户留下不专业的印象
- 信息记忆负担:大量小区数据难以完全记忆,临时查询影响体验
- 带看记录缺失:带看过程中的对话和客户需求难以完整记录
1.2 智能眼镜的优势
Rokid Glasses 为经纪人作业场景带来了创新性的解决方案:
- 设备轻量化:眼镜形态,佩戴自然,解放双手
- 智能屏显技术:信息实时呈现在眼前,无需低头查看
- 音频采集能力:实时录制带看对话,为后续分析提供数据
1.3 智眸应用概述
智眸 是贝壳找房专为经纪人打造的 AI 眼镜端应用,运行于 Rokid Glasses 眼镜设备和普通 Android 手机设备。
本项目基于 Rokid CXR-M SDK(手机端)和 CXR-S SDK(眼镜端)开发,旨在通过 AI 技术和智能眼镜赋能经纪人,提供:
- 实时小区信息提醒:眼镜端实时展示当前小区的核心数据
- 智能总结提词:结合 AI 能力进行智能总结提词
- 社区信息采集:利用Rokid Glasses设备的便携性拍摄能力,在采集过程中边走边录
- 便捷的视听辅助:解放双手,提升带看体验与作业效率
二、功能需求:信息宝典
2.1 功能名称
信息宝典
2.2 功能描述
基于 Rokid Glasses 智能眼镜的屏显技术,为经纪人提供带看中的小区楼盘信息提示功能。通过手机与眼镜的双端协同,实现:
- 小区列表展示与选择
- 实时音频采集与传输
- 服务端语音识别与信息匹配
- 眼镜端信息实时提示
2.3 功能目标
- 提升经纪人带看体验:无需频繁查看手机,信息自然呈现在眼前
- 打造专业的服务体验:解放双手,与客户保持流畅交流
- 积累带看数据资产:录制完整带看音频,为后续数据分析、AI 训练提供素材
2.4 使用场景与流程
1. 启动功能
└─> 两端建立连接
└─> 唤起"信息宝典"功能
2. 选择小区
└─> 基于定位获取附近小区列表
└─> 眼镜端展示小区列表
└─> 经纪人通过眼镜按或者手机选择目标小区,或者搜索其他小区
3. 带看过程
└─> 眼镜端开始实时录音
└─> 音频流通过 SDK 传输到手机端
└─> 手机端转发到服务端进行语音识别
└─> 服务端匹配楼盘字典信息
└─> 推送信息到手机端
└─> 手机端通过 SDK 推送到眼镜端
└─> 眼镜端实时展示信息
4. 结束带看
└─> 停止录音
└─> 上报带看统计数据
└─> 音频文件保存用于后续分析
三、技术方案
3.1 技术选型
为什么选择 CXR-M + CXR-S SDK 组合?
CXR-M SDK(手机端)核心能力:
- 与 Rokid Glasses 建立连接
- 获取眼镜端的音频流
- 与 CXR-S SDK 构建自定义通信协议
- 操作眼镜端的亮度、音量,获取眼镜基本信息等
CXR-S SDK(眼镜端)核心能力:
- 构建自定义页面
- 监听眼镜按键信息
- 与 CXR-M SDK 构建自定义通信协议
选型理由:
- 双端协同需求:信息宝典功能需要手机端和眼镜端实时双向数据同步
- 复杂交互需求:眼镜端需要使用CXR-S SDK实时监听手机端推送的数据,进行复杂的界面展示和交互
- 音频采集需求:CXR-M SDK 可直接获取眼镜端音频流,无需眼镜端额外处理
- 通信灵活性:支持自定义命令和数据传输协议,构建双端数据通信的桥梁
3.2 整体方案设计

3.3 音频采集方案:基于 CXR-M SDK 的音频流能力
3.3.1 音频流监听核心实现
CXR-M SDK 通过 DataInteractionHelper 提供了完整的音频流采集能力。SDK 内部实现了音频流监听器,负责接收眼镜端传输的音频数据并分发给业务层:
// DataInteractionHelper.kt - SDK 音频流监听核心实现
object DataInteractionHelper {
// 音频流监听实现(SDK 内部)
private val audioStreamListener by lazy {
object : AudioStreamListener {
override fun onStartAudioStream(codecType: Int, streamType: String?) {
// 分发到所有注册的回调
audioStreamCallbacks.notifyListeners { callback ->
callback.onAudioStreamStarted(codecType, streamType)
}
}
override fun onAudioStream(data: ByteArray?, offset: Int, length: Int) {
audioStreamCallbacks.notifyListeners(cleanupInvalidReferences = false) { callback ->
callback.onAudioStreamReceived(data, offset, length)
}
}
}
}
// 开启/关闭音频流监听(全局开关)
fun switchGlobalAudioStreamListenerEnable(enable: Boolean) {
CxrApi.getInstance().setAudioStreamListener(
if (enable) audioStreamListener else null
)
}
// 注册音频流回调
fun registerAudioStreamCallback(callback: CXRAudioStreamCallback) {
switchGlobalAudioStreamListenerEnable(true)
audioStreamCallbacks.addListener(callback)
}
// 注销音频流回调
fun unregisterAudioStreamCallback(callback: CXRAudioStreamCallback) {
audioStreamCallbacks.removeListener(callback)
}
// 通知眼镜开始音频采集
fun openAudioRecord(codecType: Int, streamType: String): ValueUtil.CxrStatus? {
return CxrApi.getInstance().openAudioRecord(codecType, streamType)
}
// 通知眼镜结束音频采集
fun closeAudioRecord(streamType: String): ValueUtil.CxrStatus? {
return CxrApi.getInstance().closeAudioRecord(streamType)
}
}
核心逻辑说明:
- 音频流监听器:
audioStreamListener是监听实现,负责接收眼镜端传输的音频数据 - 回调分发机制:通过
audioStreamCallbacks.notifyListeners将音频数据分发给所有注册的业务回调 - 全局开关:
switchGlobalAudioStreamListenerEnable控制音频流监听的开启和关闭 - 回调管理:支持多个业务方注册和注销音频流回调
3.3.2 拾音场景切换功能
CXR-M SDK 支持近场、远场、全场三种拾音场景切换,通过 changeAudioSceneId 方法实现:
// DataInteractionHelper.kt
fun changeAudioSceneId(sceneId: Int): ValueUtil.CxrStatus? {
return CxrApi.getInstance().changeAudioSceneId(sceneId)
}
三种拾音场景说明:
| 场景 | 说明 | 适用场景 |
|---|---|---|
| 近场 | 只能收录佩戴者的声音 | AI 语音交互(Rokid 默认模式) |
| 远场 | 只能收录正前方一定范围内的声音 | 面对面交流场景 |
| 全场 | 全局拾音 | 音视频录制、多人对话场景 |
信息宝典场景的特殊处理:
在信息宝典功能中,需要同时识别经纪人和客户的声音,因此需要将拾音模式从默认的近场切换为全场:
// 进入信息宝典功能时切换为全场拾音
fun enterInfoTreasure() {
// 切换到全场拾音模式
DataInteractionHelper.changeAudioSceneId(AUDIO_SCENE_FULL)
}
// 退出信息宝典功能时恢复近场拾音
fun exitInfoTreasure() {
// 恢复到近场拾音模式(重要!否则会影响"乐奇!"语音唤醒)
DataInteractionHelper.changeAudioSceneId(AUDIO_SCENE_NEAR)
}
⚠️ 重要提示:使用完毕后务必切换回近场模式,否则会影响 Rokid 眼镜的"乐奇!"语音唤醒功能!
四、代码实现
4.1 通信封装:RokidCommunicationHelperM/G
4.1.1 眼镜端通信助手
// RokidCommunicationHelperG.kt
object RokidCommunicationHelperG {
private val cxrServiceBridge by lazy {
CXRServiceBridge()
}
// 消息订阅机制
private fun setupMessageSubscriptions() {
val msgCallback = CXRServiceBridge.MsgCallback { name, args, value ->
handleIncomingMessage(name, args)
}
// 订阅所有相关消息类型
cxrServiceBridge.apply {
messageTypeClassMap.keys.forEach {
subscribe(it, msgCallback)
}
}
}
// 发送小区选择消息到手机端
fun sendCommunitySelected(
community: ResblockInfo,
source: String = MessageConstants.SELECT_SOURCE_GLASSES
): Boolean {
val selectedMessage = CommunitySelectedMessage(community, source)
return sendMessage(MessageConstants.MSG_COMMUNITY_SELECTED, selectedMessage)
}
// 通用消息发送方法
private fun sendMessage(cmd: String, message: BaseMessageModel): Boolean {
val jsonString = gson.toJson(message)
val args = Caps().apply {
write(jsonString)
}
val result = cxrServiceBridge.sendMessage(cmd, args)
return true
}
}
4.1.2 手机端通信助手
// RokidCommunicationHelperM.kt
class RokidCommunicationHelperM {
companion object {
fun getInstance(): RokidCommunicationHelperM {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: RokidCommunicationHelperM().also {
DataInteractionHelper.registerCustomCmdCallback(it)
it.initializeMessageHandlers()
INSTANCE = it
}
}
}
}
// 发送小区列表到眼镜端
fun sendCommunityList(
communities: List<ResblockInfo>,
title: String = "这是你附近的小区",
description: String = "你想查看第几个小区的信息"
): Boolean {
val listMessage = CommunityListMessage(communities, title, description)
return sendMessage(MessageConstants.MSG_COMMUNITY_LIST, listMessage)
}
// 发送提示信息到眼镜端
fun sendPromptInfo(prompt: CBPromptInfo): Boolean {
val promptMessage = PromptInfoMessage(prompt)
return sendMessage(MessageConstants.MSG_PROMPT_INFO, promptMessage)
}
// 通用消息发送方法
private fun sendMessage(cmd: String, message: BaseMessageModel): Boolean {
val jsonString = gson.toJson(message)
val args = Caps().apply {
if (cmd in ByteMessageCmd) {
write(jsonString.toByteArray())
} else {
write(jsonString)
}
}
val status = DataInteractionHelper.sendCustomCmd(cmd, args)
return status == ValueUtil.CxrStatus.REQUEST_SUCCEED
}
}
4.2 音频流处理:业务层实现
4.2.1 音频流接收与缓冲区管理
// CommunityPromptService.kt
class CommunityPromptService : Service(), CXRAudioStreamCallback {
private val audioBuffer = mutableListOf<Byte>()
private val audioBufferLock = Any()
private val audioBufferSize = 8 * 1024 // 8KB 阈值
override fun onAudioStreamReceived(data: ByteArray?, offset: Int, length: Int) {
data?.let {
val audioData = it.sliceArray(offset until offset + length)
// 缓冲区累积
val shouldFlush = synchronized(audioBufferLock) {
audioBuffer.addAll(audioData.toList())
audioBuffer.size >= audioBufferSize
}
// 达到阈值后异步发送
if (shouldFlush) {
serviceScope.launch {
flushAudioBuffer()
}
}
}
}
private suspend fun flushAudioBuffer() {
val audioDataToSend = synchronized(audioBufferLock) {
if (audioBuffer.isNotEmpty()) {
val audioData = audioBuffer.toByteArray()
audioBuffer.clear()
audioData
} else {
null
}
}
audioDataToSend?.let { audioData ->
// 发送到 WebSocket
audioWebSocketManager.sendAudioData(audioData)
// 本地存储
audioStreamSaveManager.addAudioData(audioData)
}
}
}
4.3 双向通信实现
4.3.1 消息订阅与分发(眼镜端)
// RokidCommunicationHelperG.kt
private fun handleIncomingMessage(cmd: String?, args: Caps?) {
if (cmd == null || args == null) return
var jsonStr: String? = null
var messageByte: ByteArray? = null
// 解析 Caps 数据
for (i in 0 until args.size()) {
args.at(i)?.let {
when (it.type()) {
Caps.Value.TYPE_STRING -> jsonStr = it.string
Caps.Value.TYPE_BINARY -> messageByte = it.binary.data
}
}
}
// JSON 反序列化
val messageBean = gson.fromJson(jsonStr, messageTypeClassMap[cmd])
messageBean?.let {
it.setBinaryData(messageByte)
MessageTypeDispatcherRegistry.dispatchMessage(it)
}
}
4.3.2 消息发送机制(手机端)
// RokidCommunicationHelperM.kt
private fun sendMessage(cmd: String, message: BaseMessageModel): Boolean {
return try {
val jsonString = gson.toJson(message)
val args = Caps().apply {
if (cmd in ByteMessageCmd) {
write(jsonString.toByteArray())
} else {
write(jsonString)
}
}
val status = DataInteractionHelper.sendCustomCmd(cmd, args)
val success = status == ValueUtil.CxrStatus.REQUEST_SUCCEED
success
} catch (e: Exception) {
false
}
}
4.4 眼镜端 UI 渲染
4.4.1 StateFlow 响应式状态管理
// CommunityBookViewModel.kt
class CommunityBookViewModel : ViewModel() {
// 状态管理
private val _currentState = MutableStateFlow(MessageConstants.STATE_LOADING)
val currentState: StateFlow<Int> = _currentState.asStateFlow()
private val _communities = MutableStateFlow<List<ResblockInfo>>(emptyList())
val communities: StateFlow<List<ResblockInfo>> = _communities.asStateFlow()
private val _answerContent = MutableStateFlow("我正在听...")
val answerContent: StateFlow<String> = _answerContent.asStateFlow()
// 小区选择处理
fun handleCommunityListReceived(message: CommunityListMessage) {
viewModelScope.launch {
_currentState.value = MessageConstants.STATE_COMMUNITY_SELECT
if (message.communities.isNotEmpty()) {
_communities.value = message.communities
_currentFocusIndex.value = 0
}
}
}
// 信息推送处理
fun handlePromptInfoReceived(message: PromptInfoMessage) {
viewModelScope.launch {
_currentState.value = MessageConstants.STATE_PROMPT_INFO
_answerContent.value = message.prompt.answer
// 增量更新逻辑
if (isNewIndex) {
_multiAnswerItems.value = listOf(answerList)
} else {
val currentItems = _multiAnswerItems.value.toMutableList()
currentItems.add(answerList)
_multiAnswerItems.value = currentItems
}
}
}
}
4.4.2 Activity 状态观察
// CommunityBookActivity.kt
class CommunityBookActivity : BaseActivity() {
private val viewModel: CommunityBookViewModel by viewModels()
private fun observeViewModel() {
// 状态观察
lifecycleScope.launch {
viewModel.currentState.collect { state ->
binding.tvLoadingView.visible(state == MessageConstants.STATE_LOADING)
binding.llSelectContainer.visible(state == MessageConstants.STATE_COMMUNITY_SELECT)
binding.llAnswerContainer.visible(state == MessageConstants.STATE_PROMPT_INFO)
}
}
// 小区列表更新
lifecycleScope.launch {
viewModel.communities.collect { communities ->
updateCommunityList(communities)
}
}
// 答案内容更新
lifecycleScope.launch {
viewModel.answerContent.collect { content ->
binding.tvAnswerContent.text = content
}
}
}
}
五、应用效果
5.1 界面展示
眼镜端界面
- 加载状态:显示"正在通过定位获取小区信息"
- 小区选择:展示附近小区列表,支持按键上下选择
- 信息提示:实时展示楼盘字典信息,支持多答案项增量展示
![]() |
![]() |
![]() |
|---|---|---|
手机端界面
- 连接状态:显示眼镜连接状态
- 服务运行:前台服务通知,显示"信息宝典运行中"
- 音频采集:实时显示录音状态
![]() |
![]() |
![]() |
|---|---|---|
5.2 用户体验反馈
经纪人反馈:
- "带看过程中不用频繁看手机了,客户觉得我更专业了"
- "眼镜上直接显示小区信息,很方便"
- "录音功能很好,回去可以复盘带看过程"
效果提升:
- 带看专业度显著提升
- 信息查阅时间大幅减少
- 客户满意度提升
六、总结与展望
6.1 项目价值
对经纪人的价值
- 提升专业形象:无需频繁查看手机,与客户保持眼神交流
- 提高作业效率:信息实时呈现,减少查阅时间
- 降低记忆负担:小区数据自动展示,专注客户服务
对平台的价值
- 积累数据资产:录制完整带看音频,为 AI 训练提供素材
- 优化服务流程:带看过程线上化,便于质量监控
- 提升品牌形象:科技创新赋能经纪人,提升品牌竞争力
6.2 SDK 使用体会
CXR-M SDK 音频流能力稳定可靠
- 音频质量好,延迟低
- 接口简单易用,集成成本低
- 支持拾音场景切换,适应不同场景需求
自定义命令通信灵活高效
- 支持 JSON 和二进制数据传输
- 传输稳定,错误处理完善
- 消息订阅机制清晰
双端协同能力强
- CXR-M 和 CXR-S 配合默契
- 通信协议自定义灵活
- 文档和示例逐步完善
建议
增加更多实际场景示例
- 双端通信完整示例
- 音频流处理最佳实践
- 拾音场景切换使用指南
优化调试工具
- 提供日志查看工具
- 增加消息抓包功能
加强社区支持
- 建立开发者社区
- 定期技术分享
6.3 未来规划
功能扩展
- 更多 AI 能力集成:语音识别、语义理解、智能问答
- 业务场景扩展:签约、过户、贷款等更多作业场景
- 数据分析能力:带看质量分析、客户需求分析
技术优化
- 性能优化:降低功耗,提升响应速度
- 体验优化:更自然的交互方式,更智能的信息推送
- 稳定性提升:异常处理、容错机制
结语
智眸应用是 Rokid AI 眼镜在房产经纪行业的创新应用实践。通过 CXR-M 和 CXR-S SDK 的双端协同,我们成功实现了眼镜端信息实时展示、音频流采集传输、服务端智能识别的完整闭环。
"镜在眼前,智慧搭伴"——这不仅是我们的 Slogan,更是我们对未来作业方式的愿景。我们相信,随着 AI 技术和智能硬件的发展,经纪人将迎来更高效、更专业、更智能的作业新时代。
技术栈:
- 眼镜端:Rokid CXR-S SDK + Kotlin
- 手机端:Rokid CXR-M SDK + Kotlin
- 服务端:语音识别 + 楼盘字典匹配
团队成员:贝壳找房 AI 眼镜项目组
本文App基于 Rokid CXR-M SDK 和 CXR-S SDK 开发,感谢 Rokid 团队提供的技术支持。
Rokid Glasses开发指南 https://ar.rokid.com/sprite?lang=zh
Rokid开发者论坛 https://forum.rokid.com/index





