会议透镜(Meeting Lens):基于 Rokid CXR-M 的 AI 会议纪要实战

在这篇文章里,我想分享一个基于 Rokid CXR-M SDK + 大模型(ASR + LLM) 打造的实验项目------「会议透镜(Meeting Lens)」。

佩戴 Rokid AI 眼镜进入会议室,你只需要正常发言、讨论、举手、点头;

会议结束时:

  • 结构化纪要(议题、结论、行动项)已经生成
  • 关键决策片段自动高亮
  • 下一步 To-do 直接推送到你的手机 / 协作工具

甚至在会议进行中,眼镜已经实时把「当前议题」「待确认事项」浮现在你眼前。

下面我会围绕 CXR-M SDK 的真实能力与接口*展开,从环境配置、设备连接、音频流采集,到端云协同的 AI 工作流,以及如何利用 提词器场景 + 自定义页面(Custom View),把实时要点和最终纪要回传到眼镜端显示。

从"做笔记"到"只开口":会议纪要的重构

1.1 传统会议纪要的瓶颈

大多数人的会议体验大概是这样的:

  • 一边听,一边记,错过别人说了什么
  • 会议结束堆满语音、照片、零散文档
  • 真正有用的是那几条:决议 & 行动项 ------ 却偏偏最难完整记录
  • 远程 / 现场混合开会时,多人同时说话,回放和标注十分痛苦

「会议透镜」想改变的是:

让"会议纪要"不再是人的负担,而是 AI 的职责。 人只负责 表达决策,记录、整理、分发全部自动完成。

1.2 为什么选择 Rokid + CXR-M SDK 来做会议助手

要做一个「只要戴上就能自动做纪要」的系统,需要同时满足:

  1. 第一视角 & 免手持

    1. 眼镜自带麦克风,天然贴近发言者
    2. 屏幕就在视野里,适合实时展示要点、提醒「还有一个行动项没确认」
  2. 端云协同能力

    1. 眼镜通过 蓝牙 / Wi-Fi 与手机建立稳定链路
    2. 手机有充足算力跑本地 ASR / 噪声处理
    3. 云端大模型负责长期记忆、多轮推理、结构化输出

Rokid 的 CXR-M(Mobile SDK) 正好提供了这一整套基础设施:

  • 蓝牙扫描与连接、状态管理
  • Wi-Fi P2P 高速链路(适合同步录音文件、会议多媒体)
  • 眼镜端音频流采集(openAudioRecord / setAudioStreamListener
  • AI 场景事件监听(AiEventListener
  • 提词器 / 翻译 / 自定义页面等场景,用于在眼镜上展示文字与 UI

对「会议透镜」这样的项目来说,最自然的架构是:

  • 眼镜: 采集 & 显示
  • 手机: 通信 + AI 工作流(ASR + LLM)
  • 云端: 大规模模型推理 & 长期存储

整体架构:端 -- 云协同的「会议透镜」

先看一眼 Meeting Lens 的数据流(加一张简易流程图方便读者理解整体链路):

scss 复制代码
[用户佩戴 Rokid 眼镜]
        │  1. 讲话 / 讨论
        ▼
[眼镜麦克风]
        │  2. 音频流(蓝牙)
        ▼
[CXR-M SDK @ 手机]
        │  3. AudioStreamListener 接收音频
        │     分段 / 预处理
        ▼
[MeetingLensManager @ 手机]
        │  4. 调用后端 /asr/stream 做实时转写
        │     累积 transcript
        │  5. 定时调用 /meeting/summarize 做中间 & 最终纪要
        ▼
[后端 AI 服务]
        │  ASR(Whisper/讯飞...) + LLM(大模型)
        │  输出:summary / 决策 / To-do
        ▼
[MeetingLensManager]
        │  6. 通过 Custom View / 提词器
        │     把纪要 & TODO 推回眼镜
        ▼
[眼镜显示层:Meeting View]
        → 实时展示议题 / 结论 / 行动项

2.1 会议现场采集

  • 佩戴 Rokid 眼镜,启动「会议助手」AI 场景
  • 眼镜端麦克风启动录音,音频流通过 CXR-M 回传手机

2.2 端侧预处理

  • 手机端对音频做降噪、VAD(分段)、可能的说话人分离
  • 分段音频送往本地 / 云端 ASR 服务(英文、中文或多语种)

2.3 云端 AI 纪要引擎

  • 基于 ASR 文本 + 说话人标签

  • 使用 LLM 做:逐段总结 + 会议中期小结 + 会后全局总结

  • 输出结构化结果:

    • 议题列表
    • 讨论过程摘要
    • 决策
    • 待办(Owner + 截止时间)

2.4 结果回传 & 实时提示

  • 会议进行中:

    • 当前议题 / 刚刚说的关键句被高亮为「候选决议」
    • 经手动确认后固化为决议
    • 通过 提词器 / Custom View 回传到眼镜显示
  • 会议结束:

    • 最终纪要发送手机 App / 邮件 / IM 群
    • 眼镜屏幕展示「本次会议 X 项关键决策 + Y 个行动项」

2.5 循环刷新

  • 每当检测到一个「新的议题块」开始(通过主持人语句、关键词或 UI 交互),重启一轮 ASR + 摘要,保证结构清晰。

开发环境与 CXR-M 集成

3.1 基础环境

  • Android Studio 2023.1+
  • Android 手机(Android 9.0+)
  • Rokid AR 眼镜(开启开发者模式)
  • JDK 8+
  • Kotlin 1.8+/1.9+(本文代码以 Kotlin 为主)

3.2 添加 Rokid Maven 仓库与 CXR-M 依赖

根据官方文档,CXR-M SDK 发布在 Rokid 自有 Maven 仓库,需要在 settings.gradle.kts 里添加:

scss 复制代码
// settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
        google()
        mavenCentral()
    }
}

app/build.gradle.kts 中引入 SDK:

ini 复制代码
android {
    compileSdk = 34
    defaultConfig {
        applicationId = "com.example.meetinglens"
        minSdk = 28        // CXR-M 要求的最小版本
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"
    }
}
​
dependencies {
    implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2")
    // Retrofit / OkHttp / Gson 等网络依赖按需添加
}

3.3 权限与动态申请

CXR-M 涉及的最小权限集包括:蓝牙、Wi-Fi、网络等,典型配置:

xml 复制代码
<!-- AndroidManifest.xml(节选) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- 录音权限,用于会议音频采集 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />

运行时建议一次性申请所有必要权限(包括 Android 12+ 的 BLUETOOTH_SCAN / BLUETOOTH_CONNECT),再初始化 CXR,否则 SDK 会直接不可用。

设备接入:用 CXR-M 打通眼镜与手机

4.1 蓝牙扫描与连接

官方通常会提供一个较完整的 BluetoothHelper 示例,用于:

  • 动态申请蓝牙相关权限
  • 检测蓝牙开关状态
  • 扫描 Rokid 眼镜(通过特定 Service UUID 过滤)
  • 维护已配对 / 已连接设备列表

在 Meeting Lens 中,我们可以复用这一套逻辑,扫描中通过 Service UUID 过滤 Rokid 设备:

scss 复制代码
// 扫描时使用 Rokid Glasses 的 Service UUID 过滤
ScanFilter.Builder()
    .setServiceUuid(
        ParcelUuid.fromString("00009100-0000-1000-8000-00805f9b34fb")
    )
    .build()

找到目标设备后,调用:

  • initBluetooth(context, device, callback) 完成初始化
  • onConnectionInfo 获得 socketUuidmacAddress,再调用 connectBluetooth() 建立通信

4.2 Wi-Fi P2P:为录音文件和附件留出通道

会议中如果需要同步完整录音文件、演示资料、截图等,可以使用 Wi-Fi P2P 模块:

  • 初始化:initWifiP2P(callback)
  • 获取状态:isWifiP2PConnected
  • 反初始化:deinitWifiP2P()

策略上:

  • 实时 ASR:走蓝牙音频流
  • 会后上传完整录音 / 媒体:走 Wi-Fi P2P + 文件同步接口(如 startSync / syncSingleFile

核心链路:从音频流到结构化会议纪要

5.1 获取眼镜端音频流:AudioStreamListener

CXR-M 提供了一套完整的录音接口:

  • 开启录音:openAudioRecord(codecType, streamType)
  • 关闭录音:closeAudioRecord(streamType)
  • 设置监听:setAudioStreamListener(callback)

示例代码:

kotlin 复制代码
// 1. 设置音频流监听
private val audioStreamListener = object : AudioStreamListener {
    override fun onStartAudioStream(codecType: Int, streamType: String?) {
        // 例如 streamType = "meeting"
        // 可以在这里重置缓冲区、时间戳等
    }
​
    override fun onAudioStream(data: ByteArray?, offset: Int, length: Int) {
        if (data == null || length <= 0) return
        // 将 PCM / Opus 数据推入本地 ASR Engine / 发送到云端
        asrEngine.feed(data, offset, length)
    }
}
​
fun startMeetingAudioStream() {
    // 注册监听
    CxrApi.getInstance().setAudioStreamListener(audioStreamListener)
​
    // 开启录音:codecType: 1=PCM, 2=OPUS(按文档约定)
    CxrApi.getInstance().openAudioRecord(
        codecType = 2,
        streamType = "meeting_assistant"
    )
}
​
fun stopMeetingAudioStream() {
    CxrApi.getInstance().closeAudioRecord("meeting_assistant")
    CxrApi.getInstance().setAudioStreamListener(null)
}

这一步完成后,我们就把「眼镜上的麦克风」变成了「AI 会议纪要系统的耳朵」。

5.2 ASR + 说话人分离:把"声"变成"谁说了什么"

在端 / 云侧,可以使用你熟悉的 ASR 引擎(如 Whisper、讯飞等):

  • onAudioStream 中的音频切成 1~2 秒片段
  • 送入流式 ASR
  • 对每段打上时间戳,并结合麦克风阵列 / 外部麦 / 会议室麦克风做说话人分离

得到的数据结构大致是:

json 复制代码
{
  "segments": [
    {
      "speaker": "Alice",
      "start": 12.3,
      "end": 18.7,
      "text": "我们这季度的核心目标是把留存提升到 35%。"
    }
  ]
}

5.3 LLM 纪要引擎:从文本到结构化结论

基于上述 segments,你可以设计多层次总结流程:

  1. 分段摘要

    1. 每 2~3 分钟做一次小节,得到"当前议题的小结"
  2. 会议中期小结

    1. 当主持人切换议题时("那下面讨论第二个问题"),触发一次自然段总结
  3. 会后总总结

    1. 聚合所有文本 + 中间小结

    2. 按如下结构输出:

      • 会议时间 / 地点 / 参与者
      • 议题列表
      • 每个议题的:背景 / 讨论过程 / 结论
      • 待办事项(Owner + Deadline + 状态)

5.4 把关键要点回传到眼镜:提词器 + 自定义页面

方式一:用提词器场景做「会议 TODO 滚动条」

CXR-M 提供了 提词器(Word Tips)场景,可以:

  • 打开 / 关闭场景:controlScene(ValueUtil.CxrSceneType.WORD_TIPS, toOpen, null)
  • sendStream(ValueUtil.CxrStreamType.WORD_TIPS, ...) 发送提词内容

例如,把已经确认的行动项转成一段清单文本推送到眼镜:

kotlin 复制代码
fun openTodosOnGlasses(text: String, fileName: String = "meeting_todos.txt") {
    // 打开提词器场景
    CxrApi.getInstance().controlScene(
        ValueUtil.CxrSceneType.WORD_TIPS,
        true,
        null
    )
​
    // 发送文字内容
    CxrApi.getInstance().sendStream(
        ValueUtil.CxrStreamType.WORD_TIPS,
        text.toByteArray(),
        fileName,
        object : SendStatusCallback {
            override fun onSendSucceed() { }
            override fun onSendFailed(code: ValueUtil.CxrSendErrorCode?) { }
        }
    )
}

再配合 configWordTipsText 调整字号、行距、显示区域,可以把「关键 TODO」像字幕一样滚动在视野边缘。

方式二:用 Custom View 做「实时会议状态卡片」

自定义页面(Custom View)允许用 JSON 描述一个界面,包含 TextView / ImageView / LinearLayout / RelativeLayout 等控件,眼镜端会根据 JSON 渲染。

典型用法:

  1. 打开界面:openCustomView(content)
  2. 更新:updateCustomView(content)
  3. 监听状态:setCustomViewListener(listener)

例如构建一个简单的「会议状态卡片」:

json 复制代码
{
  "type": "LinearLayout",
  "props": {
    "layout_width": "match_parent",
    "layout_height": "wrap_content",
    "orientation": "vertical",
    "backgroundColor": "#80000000",
    "paddingTop": "8dp",
    "paddingBottom": "8dp"
  },
  "children": [
    {
      "type": "TextView",
      "props": {
        "id": "tv_topic",
        "text": "当前议题:Q3 留存目标",
        "textSize": "14sp",
        "textColor": "#FFFFFFFF",
        "textStyle": "bold"
      }
    },
    {
      "type": "TextView",
      "props": {
        "id": "tv_highlight",
        "text": "Alice:我们需要把产品内推荐体系重构一遍。",
        "textSize": "12sp",
        "textColor": "#FFDDDDDD",
        "marginTop": "4dp"
      }
    },
    {
      "type": "TextView",
      "props": {
        "id": "tv_todos",
        "text": "行动项:3 条(点头确认可记为已认领)",
        "textSize": "12sp",
        "textColor": "#FF00FF00",
        "marginTop": "4dp"
      }
    }
  ]
}

openCustomView() 推送一次,后续只需用 updateCustomView()tv_topic / tv_highlight / tv_todostext 即可。

实战:基于 CXR-M 的 Meeting Lens 开发实践(含完整代码)

6.1 实战目标

实现一个 MeetingLensManager

  • startMeeting()

    • 开启眼镜端录音(openAudioRecord
    • 注册 AudioStreamListener 接收音频流
    • 打开 Custom View,在眼镜上显示「会议纪要生成中」
    • 启动一个定时任务,每隔 N 秒把当前文本发给后端做中间总结
  • stopMeeting()

    • 关闭录音
    • 停止定时任务
    • 把完整 transcript 发送给后端,生成最终纪要
    • 把最终纪要回传眼镜显示

6.2 MeetingLensManager 核心类(完整代码)

下面这段 Kotlin 代码可以直接复制使用,记得把 https://your.backend.com/... 换成你自己的服务地址。

scss 复制代码
package com.example.meetinglens

import android.content.Context
import android.util.Base64
import android.util.Log
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.rokid.cxr.client.m.CxrApi
import com.rokid.cxr.client.m.callback.AudioStreamListener
import com.rokid.cxr.client.m.callback.CustomViewListener
import com.rokid.cxr.client.m.utils.ValueUtil
import kotlinx.coroutines.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject

/**
 * MeetingLensManager:会议透镜核心控制类
 * - 控制录音
 * - 调用后端 ASR / LLM
 * - 将纪要结果通过 Custom View 显示在眼镜上
 */
class MeetingLensManager(
    private val context: Context,
    private val owner: LifecycleOwner
) {

    companion object {
        private const val TAG = "MeetingLens"
        private const val STREAM_TYPE = "meeting_assistant"
    }

    private val httpClient = OkHttpClient()
    private val transcriptBuffer = StringBuilder()
    private var running = false
    private var summaryJob: Job? = null

    // 1. 音频流监听器
    private val audioStreamListener = object : AudioStreamListener {
        override fun onStartAudioStream(codecType: Int, streamType: String?) {
            Log.d(TAG, "Audio stream started: codec=$codecType, type=$streamType")
            transcriptBuffer.clear()
        }

        override fun onAudioStream(data: ByteArray?, offset: Int, length: Int) {
            if (data == null || length <= 0) return
            val chunk = data.copyOfRange(offset, offset + length)
            owner.lifecycleScope.launch(Dispatchers.IO) {
                sendAudioChunkToAsr(chunk)
            }
        }
    }

    // 2. Custom View 状态监听(可选)
    private val customViewListener = object : CustomViewListener {
        override fun onOpened() {
            Log.d(TAG, "CustomView opened")
        }

        override fun onClosed() {
            Log.d(TAG, "CustomView closed")
        }

        override fun onOpenFailed(code: Int) {
            Log.e(TAG, "CustomView open failed: $code")
        }

        override fun onIconsSent() {}
        override fun onUpdated() {}
    }

    // ====================== 外部调用接口 ======================

    /** 开始一场会议 */
    fun startMeeting() {
        if (running) return
        running = true
        Log.i(TAG, "startMeeting()")

        // 1. 设置音频监听
        CxrApi.getInstance().setAudioStreamListener(audioStreamListener)

        // 2. 开启录音:2 = OPUS 编码
        CxrApi.getInstance().openAudioRecord(2, STREAM_TYPE)

        // 3. 打开眼镜端会议页面
        openMeetingView()

        // 4. 设置 Custom View 监听(调试用)
        CxrApi.getInstance().setCustomViewListener(customViewListener)

        // 5. 定时进行中间总结
        summaryJob = owner.lifecycleScope.launch(Dispatchers.IO) {
            while (running) {
                delay(15_000L) // 每 15 秒总结一次
                val text = transcriptBuffer.toString()
                if (text.isBlank()) continue
                val midSummary = summarizeOnBackend(text, final = false)
                if (midSummary.isNotBlank()) {
                    updateSummaryOnGlasses("中间小结:\n$midSummary")
                }
            }
        }

        updateSummaryOnGlasses("纪要生成中,请自然发言...")
    }

    /** 结束会议 */
    fun stopMeeting() {
        if (!running) return
        running = false
        Log.i(TAG, "stopMeeting()")

        // 1. 停止录音
        CxrApi.getInstance().closeAudioRecord(STREAM_TYPE)
        CxrApi.getInstance().setAudioStreamListener(null)

        // 2. 停止中间总结任务
        summaryJob?.cancel()
        summaryJob = null

        // 3. 调用后端生成最终纪要
        owner.lifecycleScope.launch(Dispatchers.IO) {
            val finalText = transcriptBuffer.toString()
            val finalSummary = summarizeOnBackend(finalText, final = true)
            if (finalSummary.isNotBlank()) {
                updateSummaryOnGlasses("【本次会议最终纪要】\n$finalSummary")
            } else {
                updateSummaryOnGlasses("本次会议纪要生成失败,请检查网络或后端服务")
            }
        }
    }

    // ====================== 与后端交互 ======================

    /** 将音频切片发送到 ASR 后端,获取 partial_text */
    private fun sendAudioChunkToAsr(chunk: ByteArray) {
        try {
            val base64 = Base64.encodeToString(chunk, Base64.NO_WRAP)
            val json = JSONObject().apply {
                put("audio_base64", base64)
                put("stream_type", STREAM_TYPE)
            }.toString()

            val request = Request.Builder()
                .url("https://your.backend.com/asr/stream") // TODO: 替换为你的 ASR 接口
                .post(json.toRequestBody("application/json".toMediaType()))
                .build()

            httpClient.newCall(request).execute().use { resp ->
                if (!resp.isSuccessful) return
                val body = resp.body?.string().orEmpty()
                if (body.isBlank()) return
                val obj = JSONObject(body)
                val partial = obj.optString("partial_text", "")
                if (partial.isNotBlank()) {
                    transcriptBuffer.append(partial).append('\n')
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "sendAudioChunkToAsr error: ${e.message}", e)
        }
    }

    /** 调用 LLM 后端进行纪要总结 */
    private fun summarizeOnBackend(text: String, final: Boolean): String {
        if (text.isBlank()) return ""
        return try {
            val json = JSONObject().apply {
                put("transcript", text)
                put("final", final) // final=true 使用更重模型
            }.toString()

            val request = Request.Builder()
                .url("https://your.backend.com/meeting/summarize") // TODO: 替换为你的 Summary 接口
                .post(json.toRequestBody("application/json".toMediaType()))
                .build()

            httpClient.newCall(request).execute().use { resp ->
                if (!resp.isSuccessful) return ""
                val body = resp.body?.string().orEmpty()
                if (body.isBlank()) return ""
                val obj = JSONObject(body)
                obj.optString("summary", "")
            }
        } catch (e: Exception) {
            Log.e(TAG, "summarizeOnBackend error: ${e.message}", e)
            ""
        }
    }

    // ====================== 眼镜端 UI:Custom View ======================

    /** 打开会议透镜的基础页面 */
    private fun openMeetingView() {
        val pageJson = """
        {
          "type": "LinearLayout",
          "props": {
            "id": "root_layout",
            "layout_width": "match_parent",
            "layout_height": "match_parent",
            "orientation": "vertical",
            "gravity": "top",
            "paddingTop": "40dp",
            "paddingStart": "12dp",
            "paddingEnd": "12dp",
            "backgroundColor": "#80000000"
          },
          "children": [
            {
              "type": "TextView",
              "props": {
                "id": "tv_title",
                "layout_width": "wrap_content",
                "layout_height": "wrap_content",
                "text": "会议透镜 · Meeting Lens",
                "textSize": "16sp",
                "textColor": "#FFFFFFFF",
                "textStyle": "bold",
                "marginBottom": "8dp"
              }
            },
            {
              "type": "TextView",
              "props": {
                "id": "tv_summary",
                "layout_width": "match_parent",
                "layout_height": "wrap_content",
                "text": "纪要生成中,请自然发言...",
                "textSize": "12sp",
                "textColor": "#FFDDDDDD"
              }
            },
            {
              "type": "TextView",
              "props": {
                "id": "tv_hint",
                "layout_width": "wrap_content",
                "layout_height": "wrap_content",
                "text": "提示:结束前可口头确认关键决策",
                "textSize": "10sp",
                "textColor": "#FF00FF00",
                "marginTop": "8dp"
              }
            }
          ]
        }
        """.trimIndent()

        CxrApi.getInstance().openCustomView(pageJson)
    }

    /** 更新眼镜上的纪要内容(只改 tv_summary) */
    private fun updateSummaryOnGlasses(summary: String) {
        val display = if (summary.length > 400) {
            summary.substring(0, 400) + "..."
        } else summary

        val updateJson = """
        [          {            "action": "update",            "id": "tv_summary",            "props": {              "text": ${JSONObject.quote(display)}            }          }        ]
        """.trimIndent()

        CxrApi.getInstance().updateCustomView(updateJson)
    }
}

6.3 在 Activity 中调用

在你的 Activity 里,只需要实例化并调用即可:

kotlin 复制代码
class MeetingActivity : AppCompatActivity() {

    private lateinit var meetingLensManager: MeetingLensManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_meeting)

        meetingLensManager = MeetingLensManager(this, this)

        findViewById<Button>(R.id.btnStart).setOnClickListener {
            meetingLensManager.startMeeting()
        }

        findViewById<Button>(R.id.btnStop).setOnClickListener {
            meetingLensManager.stopMeeting()
        }
    }
}

前提:

  • 已经用 CXR-M 连接上眼镜(蓝牙已连)
  • 权限已经申请完成
  • 后端的 /asr/stream/meeting/summarize 接口可用

6.4 后端接口的最小约定(方便你自己实现)

你可以先定义一个非常简单的协议:

  • POST /asr/stream

    • 请求:{ "audio_base64": "..." }
    • 响应:{ "partial_text": "我们这季度的核心目标是..." }
  • POST /meeting/summarize

    • 请求:{ "transcript": "...长文本...", "final": true/false }
    • 响应:{ "summary": "本次会议围绕 X 展开,形成 3 个决策,分别是..." }

后端可以用:

  • Python + FastAPI + Whisper + 任意大模型
  • 或者直接接云上的 ASR / LLM 服务(省事但花钱)

踩坑与工程经验

7.1 权限不全导致 SDK 静默失败

  • 蓝牙相关权限必须一次性申请:BLUETOOTH / BLUETOOTH_ADMIN / BLUETOOTH_SCAN / BLUETOOTH_CONNECT / FINE_LOCATION
  • 未授权时 CXR-M 很多接口直接不可用,建议在初始化前加一层完整检查

7.2 音频流与 ASR 的边界对齐

  • AudioStreamListener 推来的数据是连续流,需要自己切片 + 时间戳管理
  • 记得在 onStartAudioStream 重置缓冲区
  • 建议使用 10~20ms 帧长的内部队列,对接大多数 ASR 引擎更友好

7.3 Wi-Fi P2P 兼容与降级策略

  • 部分机型对 Wi-Fi Direct 支持不好,CXR 回调里可能返回 WIFI_CONNECT_FAILED

  • 建议:

    • 实时纪要全部走音频流 + 文本,不依赖 Wi-Fi
    • 会后录音备份 / 多媒体附件再尝试 Wi-Fi,同步失败则提示用户改为手机本地保存

7.4 Custom View 复杂度与性能

  • 图片不超过 128×128 px,数量不要太多(< 10 张)
  • 布局树不要太深,多用 LinearLayout / RelativeLayout 做简单排版
  • 更新时尽量用 updateCustomView 做局部更新,避免频繁 reopen

会议助手场景下,最好只在视野边缘放一个小卡片,不要铺满屏幕,以免影响用户看人 / 看 PPT。

7.5 电量与亮度、音量管理

CXR-M 提供了:

  • 亮度监听与设置:setGlassBrightness / BrightnessUpdateListener
  • 音量监听与设置:setGlassVolume / VolumeUpdateListener
  • 电量监听:setBatteryLevelUpdateListener

可以做一些自动优化,例如:

  • 进入会议后自动把亮度调到中档,减少干扰
  • 电量低于 20% 时,在眼镜端弹出小提示

小结:从「记录会议」到「为决策服务」

「会议透镜(Meeting Lens)」本质上做了三件事:

  1. CXR-M SDK眼镜的麦克风 + 显示屏 接入到手机的 AI 工作流中;
  2. ASR + LLM,把"谁在什么时间说了什么"变成"有哪些议题、结论和行动项";
  3. 提词器 / 自定义页面,把真正重要的信息适时地浮现到用户眼前。

从工程角度看,它验证了:

  • 手机 + 眼镜 可以像一个统一的「会议终端」:眼镜负责感知 & 显示,手机负责计算与联网。
  • CXR 提供的场景能力(AI 场景、提词器、翻译、自定义页面)可以很好地承载「实时会议状态」这类信息。

从体验角度看,它把开会这件事从:

「我得边听边记,生怕漏掉一个数字」

变成:

「我只需要好好讨论,剩下的交给 AI 和眼镜。」

如果你已经有一台 Rokid 眼镜和一台 Android 手机,那么你离自己的「会议透镜」其实只差几步:

  1. 用 CXR-M 把眼镜连到手机;
  2. 打开音频流,接入你熟悉的 ASR 服务;
  3. 设计一套适合你团队的 LLM 纪要 Prompt;
  4. 用提词器或 Custom View,把实时要点和最后纪要推回眼镜。
相关推荐
Victor3561 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端
缘不易1 小时前
Springboot 整合JustAuth实现gitee授权登录
spring boot·后端·gitee
Kiri霧1 小时前
Range循环和切片
前端·后端·学习·golang
WizLC1 小时前
【Java】各种IO流知识详解
java·开发语言·后端·spring·intellij idea
Victor3561 小时前
Netty(19)Netty的性能优化手段有哪些?
后端
爬山算法1 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
白宇横流学长2 小时前
基于SpringBoot实现的冬奥会科普平台设计与实现【源码+文档】
java·spring boot·后端
Python编程学习圈3 小时前
Asciinema - 终端日志记录神器,开发者的福音
后端
bing.shao3 小时前
Golang 高并发秒杀系统踩坑
开发语言·后端·golang
壹方秘境3 小时前
一款方便Java开发者在IDEA中抓包分析调试接口的插件
后端