跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Python

从零开发 AR 演讲提词器:基于 Rokid CXR-M SDK 的实战指南

从零开发 AR 演讲提词器:基于 Rokid CXR-M SDK 的实战指南 站在讲台上,数百双眼睛注视着你。你开始演讲,却发现关键时刻想不起下一句要说什么——这种场景,每个演讲者都不陌生。 传统的解决方案是在讲台上放一张稿子,或者用 PPT 做备注。但低头看稿显得不专业,看 PPT 又要扭头,容易打断演讲节奏。如果能有一个只有自己能看到的"隐形提词器",演讲就能更加从容自信。 Rokid AR…

MqEngine发布于 2026/4/6更新于 2026/5/1753K 浏览
从零开发 AR 演讲提词器:基于 Rokid CXR-M SDK 的实战指南

从零开发 AR 演讲提词器:基于 Rokid CXR-M SDK 的实战指南

站在讲台上,数百双眼睛注视着你。你开始演讲,却发现关键时刻想不起下一句要说什么——这种场景,每个演讲者都不陌生。

传统的解决方案是在讲台上放一张稿子,或者用 PPT 做备注。但低头看稿显得不专业,看 PPT 又要扭头,容易打断演讲节奏。如果能有一个只有自己能看到的"隐形提词器",演讲就能更加从容自信。

Rokid AR 眼镜恰好提供了这种可能:将提词内容无线传输到眼镜显示屏,演讲者只需自然平视,文字便清晰呈现,而台下观众毫无察觉。本文将完整记录如何利用 Rokid CXR-M SDK 从零开发这款演讲提词器应用。

文章配图

一、技术方案设计

1.1 为什么选择 AR 眼镜

在确定技术方案前,我们先对比几种提词方案:

方案

优点

缺点

纸质稿

简单、可靠

低头看稿不专业,翻页有声响

手机/平板

便携

需要低头,屏幕反光

专业提词器

效果好

设备昂贵,需要提前架设

AR 眼镜

隐蔽、便携、平视

需要设备支持

AR 眼镜的核心优势在于隐蔽性——观众完全察觉不到你在看提词,这与专业电视台主播使用的提词器效果类似,但成本和使用门槛大大降低。

1.2 为什么选择 CXR-M SDK

Rokid 提供了多套 SDK,我选择 CXR-M SDK(客户端模式)的原因:

  1. 内置提词器场景:SDK 提供了 WORD_TIPS 场景类型,专门用于文字提示,无需自己实现渲染逻辑
  2. 蓝牙直连:手机与眼镜通过蓝牙通信,无需额外中转设备
  3. 流式传输:支持文本流发送,适合分页内容
  4. 开发门槛低:纯 Android 开发,无需学习 3D 引擎
1.3 系统架构

整个应用采用三层架构:

文章配图

二、开发环境搭建

2.1 创建项目

使用 Android Studio 创建一个新项目,选择 Empty Views Activity 模板。项目配置如下:

// app/build.gradle.kts plugins { id() () } android { namespace =  compileSdk = 34 defaultConfig { applicationId =  minSdk = 28 targetSdk = 34 } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget =  } }
"com.android.application"
id
"org.jetbrains.kotlin.android"
"com.rokid.speech"
"com.rokid.speechprompter"
"17"
2.2 添加 SDK 依赖

Rokid 的 SDK 托管在其私有 Maven 仓库,需要在 settings.gradle.kts 中添加仓库地址:

// settings.gradle.kts dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://maven.rokid.com/repository/maven-public/") } } }

文章配图

然后在 app/build.gradle.kts 中添加依赖:

dependencies { // Rokid CXR-M SDK implementation("com.rokid.cxr:client-m:1.0.1-20250812.080117-2") // Android 基础库 implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") }

文章配图

2.3 配置蓝牙权限

眼镜通过蓝牙与手机通信,需要声明相关权限:

<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

注意:Android 12+ 需要动态申请 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 权限。

三、SDK 封装层实现

直接在业务代码中调用 SDK 会导致代码耦合度高、难以测试。我设计了一个 RokidGlassesManager 单例来封装所有眼镜交互逻辑。

3.1 连接管理

首先定义连接状态回调接口:

文章配图

// RokidGlassesManager.kt object RokidGlassesManager { private val cxrApi: CxrApi by lazy { CxrApi.getInstance() } private var connectionCallback: ConnectionCallback? = null interface ConnectionCallback { fun onConnecting() fun onConnected() fun onDisconnected() fun onFailed(errorMsg: String) } val isConnected: Boolean get() = cxrApi.isBluetoothConnected fun setConnectionCallback(callback: ConnectionCallback?) { this.connectionCallback = callback } }
3.2 查找已配对设备

用户在使用前需要先在系统设置中将眼镜与手机配对。我们的应用从已配对设备列表中查找 Rokid 眼镜:

fun findRokidGlasses(bluetoothAdapter: BluetoothAdapter): BluetoothDevice? { if (ActivityCompat.checkSelfPermission( bluetoothAdapter.javaClass, Manifest.permission.BLUETOOTH_CONNECT ) != PackageManager.PERMISSION_GRANTED ) { return null } return bluetoothAdapter.bondedDevices.find { it.name?.contains("Rokid", ignoreCase = true) == true || it.name?.contains("Glasses", ignoreCase = true) == true } }
3.3 建立连接

调用 SDK 的 initBluetooth 方法建立连接:

fun connectGlasses(context: Context, device: BluetoothDevice) { connectionCallback?.onConnecting() cxrApi.initBluetooth(context, device, object : BluetoothStatusCallback() { override fun onConnectionInfo( socketUuid: String?, macAddress: String?, rokidAccount: String?, glassesType: Int ) { // 连接信息获取成功 } override fun onConnected() { connectionCallback?.onConnected() } override fun onDisconnected() { connectionCallback?.onDisconnected() } override fun onFailed(errorCode: ValueUtil.CxrBluetoothErrorCode?) { connectionCallback?.onFailed(errorCode?.name ?: "连接失败") } }) }
3.4 发送内容到眼镜

这是核心功能——将提词内容发送到眼镜显示:

interface SendCallback { fun onSuccess() fun onFailed(errorMsg: String) } fun sendContent(text: String, callback: SendCallback? = null): Boolean { if (!isConnected) { callback?.onFailed("眼镜未连接") return false } // 1. 启用提词器场景 cxrApi.controlScene(ValueUtil.CxrSceneType.WORD_TIPS, true, null) // 2. 发送文本流 val status = cxrApi.sendStream( type = ValueUtil.CxrStreamType.WORD_TIPS, stream = text.toByteArray(Charsets.UTF_8), fileName = "speech.txt", cb = object : SendStatusCallback() { override fun onSendSucceed() { callback?.onSuccess() } override fun onSendFailed(errorCode: ValueUtil.CxrSendErrorCode?) { callback?.onFailed(errorCode?.name ?: "发送失败") } } ) return status == ValueUtil.CxrStatus.REQUEST_SUCCEED }

代码要点说明:

  • controlScene(WORD_TIPS, true, null) 告诉眼镜启用提词器场景
  • sendStream 将文本以 UTF-8 编码发送
  • 回调在子线程执行,更新 UI 时需要切回主线程

四、业务逻辑实现

4.1 数据模型设计

定义演讲稿的数据结构:

// Speech.kt data class Speech( val id: Int, val title: String, val content: String, val createdAt: Long = System.currentTimeMillis() )

为了演示,我创建了一个内存数据源(实际项目中可使用 Room 数据库):

object SpeechData { val speeches = listOf( Speech( id = 1, title = "年度工作汇报","各位领导、各位同事: | |大家好!2025年是我部门快速发展的一年... |""".trimMargin() ), Speech( id = 2, title = "产品发布演示", content = "今天给大家介绍我们的最新产品..." ) ) fun getSpeech(id: Int): Speech? = speeches.find { it.id == id } }
4.2 智能分页算法

这是整个应用的核心算法。演讲稿不能简单地按字符数切割,因为:

  1. 不能在句子中间断开
  2. 要保持段落的语义完整
  3. 每页内容要适中,便于阅读

我设计的分页策略是:按段落优先,单段过长时按句子分割:

// MainActivity.kt private fun splitContent(content: String): List<String> { val result = mutableListOf<String>() val lines = content.split("\n") val currentParagraph = StringBuilder() var charCount = 0 for (line in lines) { // 当当前段落超过100字符且新行会超限时,开始新页 if (charCount + line.length > 100 && currentParagraph.isNotEmpty()) { result.add(currentParagraph.toString().trim()) currentParagraph.clear() charCount = 0 } currentParagraph.append(line).append("\n") charCount += line.length } // 添加最后一段 if (currentParagraph.isNotEmpty()) { result.add(currentParagraph.toString().trim()) } return result }
4.3 构建眼镜显示格式

发送到眼镜的内容需要格式化,包含标题、页码、正文和计时:

private fun buildDisplayText(speech: Speech, paragraph: String): String { return buildString { appendLine("📝 ${speech.title}") appendLine() appendLine("────── ${currentParagraph + 1}/${paragraphs.size} ──────") appendLine() appendLine(paragraph) appendLine() appendLine("⏱ ${formatElapsedTime(elapsedTime)}") appendLine() appendLine("◀ 上页 下页 ▶") } } private fun formatElapsedTime(millis: Long): String { val minutes = millis / (1000 * 60) val seconds = (millis / 1000) % 60 return String.format("%02d:%02d", minutes, seconds) }

五、界面开发

5.1 主界面布局

主界面分为三个区域:演讲稿列表、内容显示区、眼镜控制区:

<!-- activity_main.xml --> <LinearLayout android:orientation="vertical" android:padding="16dp"> <!-- 演讲稿列表 --> <TextView text="选择演讲稿" /> <RecyclerView android:id="@+id/rvSpeeches" android:layout_weight="1" /> <!-- 内容显示区(初始隐藏) --> <CardView android:id="@+id/controlPanel" android:visibility="gone"> <TextView android:id="@+id/tvTitle" /> <TextView android:id="@+id/tvPage" /> <TextView android:id="@+id/tvTimer" /> <ScrollView> <TextView android:id="@+id/tvContent" /> </ScrollView> <!-- 翻页按钮 --> <Button android:id="@+id/btnPrev" text="◀ 上一页" /> <Button android:id="@+id/btnNext" text="下一页 ▶" /> </CardView> <!-- 眼镜控制区 --> <CardView> <Button android:id="@+id/btnConnect" text="连接眼镜" /> <Button android:id="@+id/btnSend" text="📤 发送到眼镜" /> </CardView> </LinearLayout> 
5.2 列表适配器
// SpeechAdapter.kt class SpeechAdapter( private val speeches: List<Speech>, private val onItemClickListener: (Speech) -> Unit ) : RecyclerView.Adapter<SpeechAdapter.ViewHolder>() { private var selectedPosition = -1 inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val tvTitle: TextView = view.findViewById(R.id.tvTitle) val tvPreview: TextView = view.findViewById(R.id.tvPreview) init { view.setOnClickListener { val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { selectedPosition = position onItemClickListener(speeches[position]) } } } } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val speech = speeches[position] holder.tvTitle.text = speech.title holder.tvPreview.text = speech.content.take(50) + "..." } override fun getItemCount() = speeches.size }
5.3 主 Activity 逻辑

在 Activity 中串联所有组件:

class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private var currentSpeech: Speech? = null private var currentParagraph = 0 private var paragraphs: List<String> = emptyList() private var isRunning = false private var startTime: Long = 0 // 翻页防抖 private var lastPageChangeTime = 0L override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setupSpeechList() setupButtons() checkPermissions() observeConnection() } private fun loadSpeech(speech: Speech) { currentSpeech = speech currentParagraph = 0 paragraphs = splitContent(speech.content) updateDisplay() binding.controlPanel.visibility = View.VISIBLE } private fun previousParagraph() { if (currentParagraph > 0) { currentParagraph-- updateDisplay() if (RokidGlassesManager.isConnected) { sendToGlasses() } } } private fun nextParagraph() { if (currentParagraph < paragraphs.size - 1) { currentParagraph++ updateDisplay() if (RokidGlassesManager.isConnected) { sendToGlasses() } } } private fun sendToGlasses() { val speech = currentSpeech ?: return val paragraph = paragraphs.getOrNull(currentParagraph) ?: return val displayText = buildDisplayText(speech, paragraph) RokidGlassesManager.sendContent(displayText, object : RokidGlassesManager.SendCallback { override fun onSuccess() { runOnUiThread { Toast.makeText(this@MainActivity, "已同步到眼镜", Toast.LENGTH_SHORT).show() } } override fun onFailed(errorMsg: String) { runOnUiThread { Toast.makeText(this@MainActivity, "发送失败: $errorMsg", Toast.LENGTH_SHORT).show() } } }) } }

六、开发中的踩坑与解决

6.1 翻页防抖

问题:用户快速点击翻页按钮会导致跳过多页。

解决:添加时间间隔判断:

private var lastPageChangeTime = 0L binding.btnNext.setOnClickListener { if (System.currentTimeMillis() - lastPageChangeTime > 300) { lastPageChangeTime = System.currentTimeMillis() nextParagraph() } }
6.2 演讲时屏幕常亮

问题:演讲过程中手机自动休眠,导致蓝牙断开。

解决:在 Activity 中添加标志:

override fun onCreate(savedInstanceState: Bundle?) { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // ... }
6.3 权限动态申请

问题:Android 12+ 需要动态申请蓝牙权限。

解决:

private fun checkPermissions() { val permissions = mutableListOf<String>() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { permissions.add(Manifest.permission.BLUETOOTH_SCAN) permissions.add(Manifest.permission.BLUETOOTH_CONNECT) } val notGranted = permissions.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED } if (notGranted.isNotEmpty()) { ActivityCompat.requestPermissions(this, notGranted.toTypedArray(), 100) } }
6.4 回调线程问题

问题:SDK 回调在子线程执行,直接更新 UI 会崩溃。

解决:使用 runOnUiThread 切换线程:

override fun onConnected() { runOnUiThread { binding.btnConnect.text = "断开连接" Toast.makeText(this@MainActivity, "眼镜已连接", Toast.LENGTH_SHORT).show() } }

七、效果演示

7.1 使用流程
  1. 打开应用,从列表选择要演讲的稿件
  2. 点击「连接眼镜」,等待配对的 Rokid 眼镜连接成功
  3. 点击「开始演讲」,计时器启动
  4. 演讲过程中点击「上一页」「下一页」控制内容
  5. 内容自动同步到眼镜,平视即可看到提词
7.2 眼镜端显示效果
┌──────────────────────────────┐ │ 📝 年度工作汇报 │ │ │ │ ────── 1/5 ────── │ │ │ │ 各位领导、各位同事: │ │ │ │ 大家好!2025年是我部门 │ │ 快速发展的一年,在全体成员 │ │ 的共同努力下,我们取得了 │ │ 显著的成绩... │ │ │ │ ⏱ 02:35 │ │ │ │ ◀ 上页 下页 ▶ │ └──────────────────────────────┘

八、功能清单

功能

状态

说明

演讲稿管理

✅

列表展示、选择切换

智能分页

✅

保持段落语义完整

翻页控制

✅

上一页/下一页,带防抖

眼镜连接

✅

蓝牙配对、状态监听

内容同步

✅

实时发送到眼镜

计时功能

✅

开始/暂停,显示已用时间

九、后续改进方向

当前版本已实现核心功能,后续可考虑增强:

  1. 语音翻页:集成语音识别,说"下一页"自动翻页,双手彻底解放
  2. 时间预警:设定预计时长,超时震动提醒
  3. 文档导入:支持导入 Word、PDF、TXT 文件
  4. 云端同步:演讲稿云端存储,多设备共享
  5. 演讲分析:记录翻页节奏,分析演讲速度

十、总结

这款演讲提词器利用 Rokid AR 眼镜的 WORD_TIPS 场景,实现了"隐形提词"的效果。整个开发过程让我对 CXR-M SDK 有了深入理解:

  • 场景化设计:SDK 预置的场景类型降低了开发难度,开发者只需关注业务逻辑
  • 流式传输:sendStream API 设计简洁,适合分页内容的实时推送
  • 连接管理:蓝牙连接的复杂性被封装在 SDK 内部,回调机制清晰

AR 眼镜在办公场景有着广阔的应用空间。除了演讲提词,还可以用于实时翻译、会议记录、远程协助等场景。希望这篇文章能给其他开发者带来启发,探索更多 AR 应用的可能性。


项目源码:SpeechPrompter/

参考资源:

  • CXR-M SDK 官方文档
  • Rokid 开发者论坛

目录

  1. 从零开发 AR 演讲提词器:基于 Rokid CXR-M SDK 的实战指南
  2. 一、技术方案设计
  3. 1.1 为什么选择 AR 眼镜
  4. 1.2 为什么选择 CXR-M SDK
  5. 1.3 系统架构
  6. 二、开发环境搭建
  7. 2.1 创建项目
  8. 2.2 添加 SDK 依赖
  9. 2.3 配置蓝牙权限
  10. 三、SDK 封装层实现
  11. 3.1 连接管理
  12. 3.2 查找已配对设备
  13. 3.3 建立连接
  14. 3.4 发送内容到眼镜
  15. 四、业务逻辑实现
  16. 4.1 数据模型设计
  17. 4.2 智能分页算法
  18. 4.3 构建眼镜显示格式
  19. 五、界面开发
  20. 5.1 主界面布局
  21. 5.2 列表适配器
  22. 5.3 主 Activity 逻辑
  23. 六、开发中的踩坑与解决
  24. 6.1 翻页防抖
  25. 6.2 演讲时屏幕常亮
  26. 6.3 权限动态申请
  27. 6.4 回调线程问题
  28. 七、效果演示
  29. 7.1 使用流程
  30. 7.2 眼镜端显示效果
  31. 八、功能清单
  32. 九、后续改进方向
  33. 十、总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Java 结合 HanLP 分析各省旅游宣传口号相似度
  • 从零构建智能语音交互:Python 语音客户端实战指南
  • 汽车雷达多径效应下的幽灵目标检测技术解析
  • C++ 算法题解析:猫和老鼠(最短路与安全路径判定)
  • 使用 json-repair 库修复大模型返回的异常 JSON 格式
  • Xilinx Vivado 2020 版本安装流程及常见问题解析
  • Stable Diffusion 整合包 v4.10 与 ComfyUI 使用指南
  • AI 智能体驾驭工程(Harness Engineering)核心解析
  • C++ STL 常用容器详解与实战技巧
  • Paperiii AI 学术写作工具功能概览
  • 裸金属到实时系统:C++ 内核稳定运行的关键控制点
  • Python 核心语法详解:变量、流程控制、函数与数据结构
  • 嵌入式计算系统高效深度学习基础设施综述
  • ZeroClaw:零开销全 Rust 自主 AI 助手基础设施,与 OpenClaw 对比
  • MySQL 5.7 解压版安装与配置实战
  • 阿里巴巴 Java 开发规约 IDEA 插件安装与使用指南
  • 数据结构复习:带头双向循环链表实现详解
  • 25 个降低 AI 检测率提示词:角色设定与语义重构技巧
  • 云端多模型并行部署与写作能力对比测试方案
  • Whisper 驱动的多语种交互异常检测框架实战

相关免费在线工具

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online