机器人和你说相声,它逗哏,你拍背捧哏。本文记录 crosstalk(人机相声搭档)的开发过程:TTS 没有语速参数的情况下节奏如何控、2.5 秒捧哏窗口的两条走向、以及一个在三个技能里犯了四次的 ROS2 定时器错误。全部逻辑经 --sim 验证后打包 .rpk 部署上板。

代码仓库地址: crosstalk:基于 Python 和 TTS 的交互式相声机器人项目 - AtomGit

目录

  • 一、选题:一个会接话的技能

  • 二、演出规则:五种状态和一张表

  • 三、为什么应该把段子设计为数据

  • 四、在官方预设动作上二次调整

  • 五、节奏控制:没有语速参数的引擎

  • 六、--sim 验证:一次四犯的定时器错误

  • 七、部署上板与发布

  • 八、常见问题排查

  • 九、已知限制

  • 结语 / 参考资料

一、选题:一个会接话的技能

前两个技能分别占了数据(robot_checkup)和视觉(color_seek)两个维度,互动维度上 touch_game 已经验证了“触摸事件流 + 多模态反馈”的链路。第三个技能我想从全新的角度:让用户从“操作者”变成“表演者”。

相声这个形式几乎是为此量身定做的——它有固定的捧哏结构(逗哏说一句,捧哏接一句),接不接话是两种完全不同的走向,天然适合“机器人说、人拍背接”的交互。而且它对硬件的要求低到只需要 TTS、表情、灯带三个已经在用的通道,零运动风险。

最终玩法:机器人逗哏说原创相声,每句台词后留 2.5 秒捧哏窗口,拍背 = 接住,不拍 = 它自己吐槽,谢幕统计你的捧哏专业度。

照例交代一下地基。WSL2 + Ubuntu 22.04 + ROS2 Humble 的环境、t1-sdk 四个包的编译、URDF 模型在 RViz/Gazebo 里的加载验证,都是前两个技能留下的存量,crosstalk 开工时零环境成本:

t1-sdk 在 WSL 里 colcon build:4 packages finished——crosstalk 用到的 PlayTts、PlayEmotion、LedStripCommand 服务定义全在这个包里。

RViz 里的 T1 Pro 模型与关节列表。crosstalk 虽然不碰运动,但表情和灯带都挂在模型同一条状态链路上,模型能加载说明状态通道是通的。

更要紧的是通道级验证:TTS、表情、灯带三个接口在 touch_game 里已经实打实调过几百次,超时兜底、is_interrupted 打断了这些细节都有现成答案。crosstalk 的技术风险在动工前就只剩下“节奏怎么控”。

二、演出规则:五种状态和一张表

阶段触摸行为
待机背部单击开演(随机或指定段子)
说词中台词结束后 2.5 秒内拍背接住了——计数 +1,机器人随机夸奖
说词中不拍它自己吐槽(“行吧,我自己来”),继续说
说词中乱拍忽略——防误触,不打断正在说的句子
谢幕背部单击返场重讲
谢幕胸部双击散场

核心机制:

crosstalk 捧哏窗口与两条走向:窗口内拍背 = 接住(计数+夸奖),窗口内不拍 = 没接(吐槽也是包袱),台词播放中乱拍直接忽略。

窗口判定全部收在 on_touch 一个函数里,按相位分流:

def on_touch(self, msg):
    with self._lock:
        if self.phase == PHASE_APPLAUSE:
            if (msg.event_type == EVENT_SINGLE_CLICK
                    and msg.sensor_id == SENSOR_BACK):
                self._catch()
            return
        if self.phase == PHASE_IDLE:
            if (msg.event_type == EVENT_SINGLE_CLICK
                    and msg.sensor_id == SENSOR_BACK):
                self.start_show()
            return
        if self.phase == PHASE_DONE:
            if msg.event_type == EVENT_SINGLE_CLICK:
                self.bot.say("还要听?行,返场!")
                self._restart()
            elif (msg.event_type == EVENT_DOUBLE_CLICK
                    and msg.sensor_id == SENSOR_CHEST):
                self.bot.say("谢谢大家,我回去杵着了")
                self.bot.play_led(0)
            return

四个相位四种待遇:SPEAKING 相位什么都不接(乱拍忽略,防误触不打断台词)、APPLAUSE 相位只认背部单击、IDLE 等开演、DONE 等返场或散场。加锁是因为 TTS 回调和触摸回调可能同时进,相位读改要互斥。

“不接就吐槽”是这个技能的设计核心——冷场是演出事故,吐槽是兜底包袱。哪怕全程没人拍背,机器人也会自己把节奏走下去,谢幕时统计“这一段你捧了 0 次,专业程度百分之 0”,吐槽感拉满,反而成了效果最好的结局。

三、为什么应该把段子设计为数据

两段相声的文本全部放在 src/script_data.py 里,每条台词是一个字典:

{
    "t": "主人给我定的 KPI,就三个字——别摔了",
    "wait": True,       # 这句之后进捧哏窗口
    "emo": 18,          # 播这句时的表情
    "led": "white_breath",
}

如果需要加一段新相声,那么不需要动引擎。这个分离带来的直接好处是:段子可以随时改、随时加,而节奏逻辑只在一处维护。真实的数据长这样:

SCRIPTS = {
    "motian": {
        "title": "机器人也想摸鱼",
        "lines": [
            {"t": "大家好,我是这台桌子上的机器人", "wait": True,
             "emo": 10, "led": 20},
            {"t": "别的机器人满世界跑,我负责在这儿杵着", "wait": True},
            {"t": "主人给我定的 KPI,就三个字——别摔了", "wait": True,
             "emo": 260},
            {"t": "我唯一的激动时刻,是充电", "wait": True},
            {"t": "电量从百分之十九跳到二十,我恨不得给自己鼓掌",
             "wait": True, "emo": 19, "led": 1},
            {"t": "谢谢大家,我回去杵着了", "wait": False, "end": True},
        ],
    },
}

原则写在 script_data.py 的模块注释里:机器人视角自嘲,包袱落在“机器人的物理局限与社会身份错位”上

crosstalk 的三层结构:数据层(段子文本)、节奏引擎(时间戳推进的相位机)、执行层(TTS/表情/灯带),三层之间只靠列表和接口通信。

两段原创段子随仓库开源:

  • 《机器人也想摸鱼》——“主人给我定的 KPI 就三个字:别摔了”“电量跳动是我唯一的激动时刻”

  • 《聪明反被聪明误》——“我懂八国语言的问候,就只会一句收到收到”“聪明是直接报数据”

四、在官方预设动作上二次调整

PrimeConsole 的动作面板里有 14 项官方预设(打招呼、握手、比心、站起、变形……),看起来“让机器人表演”直接调预设就行。我们评估之后的结论是预设动作解决不了这个场景:

维度官方预设动作自写台词引擎
内容固定动画,文本写死任意文本,改列表就行
节奏一段完整播放,不可拆逐句可控,句间任意间隔
互动播放中不可插入夸奖、返场随时插播
表情配合动画自带每句可配不同表情 ID

相声需要的是台词级控制——每一句说什么、停顿多久、配什么表情,预设动画给不了。所以我们只用预设动作体系之外的东西:TTS + PlayEmotion + LedStripCommand,三个都是已在 touch_game 里验证过的通道,风险为零。

五、节奏控制:没有语速参数的引擎

相声的灵魂是节奏,而 SDK 的 TTS 接口没有语速参数——PlayTtsRequest 里只有文本、优先级和是否打断。下面是节奏设计:

  1. 每句独立 TTS 调用——句与句之间由代码控制间隔,而不是让 TTS 一口气播完

  2. 句间间隔按字数估时——间隔 = 字数 ÷ 4.5 字每秒 + 0.9s 缓冲,4.5 是经验值,真机上跑几遍就能标定准

  3. 捧哏窗口固定 2.5s——估时只影响“下一句什么时候开始”,不影响窗口本身的判定

整个时间线由一个 5Hz 的心跳按时间戳推进,相位机 SPEAKING → APPLAUSE → SPEAKING → FINISHED。核心只有 _speak_next:

def _speak_next(self):
    line = self.script[self._line_idx]
    self._line_idx += 1
    self.bot.say(line["t"])
    self.bot.play_emotion(line.get("emo"))
    self.bot.play_led(line.get("led"))
​
    if line.get("end"):
        self._finish()
        return
    tts_s = max(1.5, len(line["t"]) / 4.5)
    if line.get("wait"):
        self.phase = PHASE_APPLAUSE
        self._deadline = time.monotonic() + tts_s + self.applause_window
    else:
        self.phase = PHASE_SPEAKING
        self._next_line_at = time.monotonic() + tts_s + 0.4

max(1.5, ...) 是短句下限——四个字的短句按公式算出来不到 1 秒,捧哏还没反应过来窗口就关了,演出节奏直接垮掉。这里又一次用上了 touch_game 的教训:不用周期定时器做状态转换,只用心跳读时间戳。

is_interrupted=True 是——夸奖语、返场、散场随时可能插进来,不能让上一句排队等着播完。

六、--sim 验证:一次四犯的定时器错误

--sim 注入拍背事件走完整流程。第一次运行就撞上了那个熟悉的坑——_run_sim 注册了心跳和注入定时器,但忘了进 spin:

self.node.create_timer(1.0 / TICK_HZ, self._tick)
self.start_show()
self.node.create_timer(3.0, self._sim_applause)
self.node.create_timer(20.0, self._sim_done)
# 然后 run() 返回,main 里 shutdown——一个定时器都没执行

这是同一个错误在三个技能里的第四次发作(touch_reply 两次、touch_game 一次、crosstalk 一次)。修复一行:_run_sim 末尾补 rclpy.spin()。这次我们没再靠“记住教训”——把它写成了流程:所有 sim 入口的验收清单里加了一条“确认 timer 注册后有 spin 驱动”。

--dry-run 输出:段子《聪明反被聪明误》12 句、捧哏窗口 2.5s、背部单击 = 接话/下一段、胸部双击 = 退出——部署前的行为契约。

--sim 全程实录:12 句台词逐句播报,拍背接话 6 次全部 caught,夸奖、表情、灯带联动准确,谢幕统计后干净退出。

七、部署上板与发布

部署流程和前两个技能完全一致,这次一次通过——cfg/pkg.yaml 的教训已经记进检查清单:

python3 pack_rpk.py     # crosstalk-1.0.0.rpk,8 files

打包输出:包大小与文件数。

包内容:manifest.json(4 项能力)、cfg/pkg.yaml(技能身份)、assets/icon.png、META-INF/checksums.json 加源码五件套。

PrimeConsole 解析结果:skillid = crosstalk,原子能力三项(表情播放 / 语音合成 / LED 灯效)。

部署成功:crosstalk-1.0.0.rpk 安装到机器人,界面显示“部署成功”。

部署后的 PrimeConsole 主界面:机器人三维模型与实时视频同屏,部署链路对机器人本体的状态可见。

发布表单也走同一套元数据:

PrimeHub 发布表单:技能名称“人机相声搭档”,分类视觉/语言/动作,标签卧室/亲子/客厅——分类与标签多选,覆盖家庭场景的检索词。

八、常见问题排查

现象原因处理
拍背没反应拍在台词播放中,不在窗口期等台词结束、灯带给出提示再拍
夸奖语不播TTS 上一句还没播完,新请求排队确认 is_interrupted=True
下一句迟迟不来台词估时偏长调小 4.5 字/秒 这个经验值
谢幕统计次数不对窗口期判定用了事件到达时刻而非相位起点检查心跳 tick 的时间戳来源
--sim 打完招呼就退出又是没进 spin验收清单第一条

九、已知限制

  • 台词时长按 4.5 字/秒估算,真人 TTS 语速有快有慢,估偏了捧哏窗口起点会前移或后退,需要真机标定

  • 拍背必须落在窗口期,播放中拍会被忽略——节奏感是玩法的一部分,但确实会让人觉得“我明明拍了”

  • 全部实时 TTS,没有预录音频,TTS 服务的延迟直接体现在演出节奏上

结语

crosstalk 是三个技能里“技术最少、表演最多”的一个:没有视觉、没有运动,全部技术含量集中在节奏引擎那几十行。但它回答了另一个问题——机器人除了“听话干活”,还能不能“陪你玩”。段子写在数据里,改文本就能加新活儿;捧哏窗口只有 2.5 秒,但这两秒里人是真的在跟机器互动。

本文使用的 SDK 与消息包均来自启元机器人开发者社区,TTS 与触摸接口以官方 t1-sdk 仓库定义为准。

参考资料

  1. 启元机器人开发者社区(SDK/文档下载):启元机器人开发者社区 · 一站式具身智能研发平台

  2. 官方 t1-sdk 仓库(含 aimdk_msgs 消息包):t1-sdk:基于 ROS2 与 FastDDS 的机器人二次开发 SDK 项目 - AtomGit

  3. 本文配套仓库 crosstalk:crosstalk:基于 Python 和 TTS 的交互式相声机器人项目 - AtomGit

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐