会议透镜(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,把实时要点和最后纪要推回眼镜。
相关推荐
课程xingkeit与top1 小时前
高性能多级网关与多级缓存架构落地实战(超清完结)
后端
课程xingkeit与top1 小时前
SpringBoot2 仿B站高性能前端+后端项目(完结)
后端
课程xingkeit与top1 小时前
AI Agent智能应用从0到1定制开发(完结)
后端
Carve_the_Code1 小时前
分布式订单系统:订单号编码设计实战
java·后端
Home1 小时前
23种设计模式之代理模式(结构型模式二)
java·后端
落枫591 小时前
OncePerRequestFilter
后端
程序员西西1 小时前
详细介绍Spring Boot中用到的JSON序列化技术?
java·后端
课程xingkeit与top1 小时前
大数据硬核技能进阶:Spark3实战智能物业运营系统(完结)
后端
课程xingkeit与top1 小时前
基于C++从0到1手写Linux高性能网络编程框架(超清)
后端