Reachy Mini Python SDK 开发实战:从运动控制到视觉音频交互

阅读

Notebook 0 — First Connection & Movement

🎯 目标:连接 Reachy Mini 并执行你的第一条运动指令。

架构概述

Reachy Mini 采用客户端-服务器架构

graph LR
    subgraph Client
        A[Your Python Script]
    end
    
    subgraph Server
        B[Reachy Daemon]
    end
    
    subgraph Reachy Mini
        C[Robot Hardware or Simulation]
    end
    
    A <--> B
    B --> C

核心概念:

  • 守护程序(Daemon):一个后台服务程序,直接控制机器人的电机、传感器、摄像头和音频。
  • Python SDK:你用来发送指令的 reachy_mini 软件包。

为什么采用这种架构?

  • 多个客户端可以同时连接(如网页应用、脚本、Jupyter Notebook)。
  • 守护程序负责安全地处理底层硬件操作。
  • 你可以通过网络远程控制机器人。例如,在与机器人连接的树莓派(Raspberry Pi)上运行守护程序,同时在性能强大的服务器上运行你的 AI 代码。

验证连接

在运行代码之前,请先确认机器人已启动并正常运行。

你应该使用 Reachy Mini Control 来检查机器人是否已连接并准备就绪。Reachy Mini Control 是一款桌面应用程序,可让你管理机器人、运行应用、播放表情以及控制其音响系统。如果尚未下载,请从官方网站下载。

在 Reachy Mini Control 中连接成功后,请确保机器人处于“开启(ON)”状态。

在继续操作前,务必确认机器人已开启!尤其是使用无线版本时,在通电后,机器人代码默认处于关闭状态。

如需排查问题,请参考文档中的“连接与 Reachy Mini Control”部分。

首次连接

让我们使用 Reachy 的 Python SDK 连接到机器人!

# 导入 ReachyMini 类
from reachy_mini import ReachyMini

# 连接机器人
# 注:media_backend="no_media" 暂时禁用摄像头/音频,下一教程讲解
with ReachyMini() as mini:
    print("成功连接 Reachy Mini!")
    print(f"机器人名称:{mini.robot_name}")

最佳实践

  • 始终使用 with 语句

    with ReachyMini() as mini:
        # 在这里编写你的代码
    # 连接在此处自动关闭
    
  • 实验时从较长的持续时间开始(1-2 秒)

    • 慢速运动更安全
    • 熟悉后可逐渐加快速度
  • 先测试小幅运动

    • 从小角度开始(5-10 度)
    • 随着对限制的了解,逐步增加活动范围
  • 留意机器人状态

    • 观察是否有意外行为
    • 确保工作空间畅通无阻
  • 在仿真环境中尝试

    • 你也可以在仿真环境中与 Reachy Mini 互动!
    • 只需在终端中用 sim 参数实例化守护程序:reachy-mini-daemon --sim。这将打开一个 MuJoCo 仿真窗口,你可以在其中查看和控制 Reachy Mini,而无需实体硬件。这是在真实机器人上运行代码之前进行安全实验和测试的好方法。更多关于仿真的信息,请参阅文档中的仿真部分

首次运动

让 Reachy Mini 动起来吧!我们将从一个简单的表情丰富的姿势开始,然后回到中立位置——这是一个安全、居中的姿势,你将在所有笔记本中将其作为参考基准。

from reachy_mini.utils import create_head_pose

with ReachyMini() as mini:
    # 好奇姿态:头部倾斜、天线张开
    print("切换至好奇姿态...")
    mini.goto_target(
        head=create_head_pose(roll=20, degrees=True),  # 头部右倾
        antennas=[0.3, -0.3],  # 天线向两侧张开
        duration=2.0,
    )

    # 回归中立位:头部朝前,天线竖直向上
    print("回归中立姿态...")
    mini.goto_target(
        head=create_head_pose(),  # 头部中立(全0)
        antennas=[0.0, 0.0],  # 天线中立(弧度)
        duration=2.0,
    )

    print("运动完成!")
  • goto_target():将机器人从当前位置平滑地移动到目标位置
  • create_head_pose(roll=20, degrees=True):创建一个头部向右倾斜 20 度的姿势。degrees=True 参数让你可以直接使用度数而非弧度。
  • antennas=[0.3, -0.3]:将两根天线向外展开(右侧天线向内为正 +,左侧天线向内为负 -
  • create_head_pose():不带参数调用时,创建中立姿势(x=0, y=0, z=0, roll=0, pitch=0, yaw=0)—— 头部直视前方
  • antennas=[0.0, 0.0]:两根天线竖直向上 —— 中立天线位置
  • duration=2.0:动作持续 2 秒。时间越长 = 越慢越平滑;时间越短 = 越快

⚠️ 提示:在每次运动之间回到中立位置是个好习惯,这样可以为你提供可预测的起始点,并确保机器人安全。

头部运动

头部有 6 个自由度

  • 平移(translation):
    • X 轴
    • Y 轴
    • Z 轴
  • 旋转(rotation):
    • Roll(翻滚)
    • Pitch(俯仰)
    • Yaw(偏航)

平移(Translation)

头部可平移

  • X 轴:前后
  • Y 轴:左右(右→左)
  • Z 轴:上下(下→上)

# 导入 ReachyMini 类
from reachy_mini import ReachyMini
from reachy_mini.utils import create_head_pose

# 连接机器人
with ReachyMini() as mini:
    # 从中立开始
    mini.goto_target(head=create_head_pose(), antennas=[0.0, 0.0], duration=1.0)

    # X轴 前后平移
    print("头部向后平移...")
    mini.goto_target(head=create_head_pose(x=-0.02), duration=1.0)
    print("头部向前平移...")
    mini.goto_target(head=create_head_pose(x=0.02), duration=1.0)
    mini.goto_target(head=create_head_pose(), duration=1.0)

    # Y轴 左右平移
    print("头部向右平移...")
    mini.goto_target(head=create_head_pose(y=-0.02), duration=1.0)
    print("头部向左平移...")
    mini.goto_target(head=create_head_pose(y=0.02), duration=1.0)
    mini.goto_target(head=create_head_pose(), duration=1.0)

    # Z轴 上下平移
    print("头部向下平移...")
    mini.goto_target(head=create_head_pose(z=-0.02), duration=1.0)
    print("头部向上平移...")
    mini.goto_target(head=create_head_pose(z=0.02), duration=1.0)
    mini.goto_target(head=create_head_pose(), duration=1.0)

    print("平移运动完成!")
  • goto_target():让机器人从当前位置平滑移动到目标位置
  • create_head_pose():根据你设定的参数,创建你想要的头部姿态。在这里只修改了平移数值(x、y 或 z)。当然,你也可以同时修改所有参数

旋转(Rotation)

头部可沿 3 个轴进行旋转:

  • Roll(翻滚):(绕 X 轴旋转)
    • 正值(➕) = 右倾
    • 负值(➖) = 左倾
  • Pitch(俯仰):(绕 Y 轴旋转)
    • 正值(➕) = 低头
    • 负值(➖) = 抬头
  • Yaw(偏航):(绕 Z 轴旋转)
    • 正值(➕) = 向左看
    • 负值(➖) = 向右看

# 导入 ReachyMini 类
from reachy_mini import ReachyMini
from reachy_mini.utils import create_head_pose

# 连接机器人
with ReachyMini() as mini:
    mini.goto_target(head=create_head_pose(), antennas=[0.0, 0.0], duration=1.0)

    # Roll 左右倾斜
    print("头部右倾...")
    mini.goto_target(head=create_head_pose(roll=20, degrees=True), duration=1.0)
    mini.goto_target(head=create_head_pose(), duration=1.0)
    print("头部左倾...")
    mini.goto_target(head=create_head_pose(roll=-20, degrees=True), duration=1.0)
    mini.goto_target(head=create_head_pose(), duration=1.0)

    # Pitch 上下点头
    print("低头...")
    mini.goto_target(head=create_head_pose(pitch=15, degrees=True), duration=1.0)
    print("抬头...")
    mini.goto_target(head=create_head_pose(pitch=-15, degrees=True), duration=1.0)
    mini.goto_target(head=create_head_pose(), duration=1.0)

    # Yaw 左右摇头
    print("向左摇头...")
    mini.goto_target(head=create_head_pose(yaw=30, degrees=True), duration=1.0)
    print("向右摇头...")
    mini.goto_target(head=create_head_pose(yaw=-30, degrees=True), duration=1.0)
    mini.goto_target(head=create_head_pose(), duration=1.0)

    print("旋转运动完成!")

⚠️ 在处理角度时,务必使用 degrees=True

Reachy Mini 设有物理和软件上的限制,以防止自碰撞和损坏。SDK 会自动将数值限制在最近的有效位置。例如,翻滚角(roll)的限制为 ±40 度,这意味着如果你尝试将翻滚角设为 50 度,它会被自动限制为 40 度。完整的运动范围请参见文档核心概念部分中的说明。

移动天线(Antennas)

天线非常适合用来表达情感和注意力!

import numpy as np

# 导入 ReachyMini 类
from reachy_mini import ReachyMini
from reachy_mini.utils import create_head_pose

# 连接机器人
with ReachyMini() as mini:
    # 从中立开始
    mini.goto_target(head=create_head_pose(), antennas=[0.0, 0.0], duration=1.0)

    # Both antennas outward (excited/alert)
    # 两个天线都向外(兴奋/警觉)
    print("Excited!")
    for _ in range(5):
        mini.goto_target(
            head=create_head_pose(),
            antennas=[
                np.deg2rad(-10),
                np.deg2rad(10),
            ],  # Right outward (-), Left outward (+) # 右向外(负值),左向外(正值)
            duration=0.1,
        )
        mini.goto_target(
            head=create_head_pose(),
            antennas=[
                np.deg2rad(10),
                np.deg2rad(-10),
            ],  # Right outward (+), Left outward (-) # 右向外(正值),左向外(负值)
            duration=0.1,
        )

    # Both antennas outward (sad/tired)
    # 两个天线都向外(悲伤/疲惫)
    print("Sad...")
    mini.goto_target(
        head=create_head_pose(),
        antennas=[
            np.deg2rad(-140),
            np.deg2rad(140),
        ],  # Right outward (-), Left outward (+) # 右向外(负值),左向外(正值)
        duration=3.0,
    )

    # 回到中立
    mini.goto_target(head=create_head_pose(), antennas=[0.0, 0.0], duration=1.0)

    # Alternating (thinking/confused)
    # 交替(思考/困惑)
    print("Confused?")
    mini.goto_target(
        head=create_head_pose(),
        antennas=[np.deg2rad(-60), np.deg2rad(-30)],
        duration=1.0,
    )
    # Right out, left in - we don't put the same value to create the confusion effect
    # 右向外,左向内 - 我们不使用相同的数值以营造困惑效果

    # 回到中立
    mini.goto_target(head=create_head_pose(), antennas=[0.0, 0.0], duration=1.0)

    print("Done!")
  • 天线以[右, 左]的格式、用弧度制指定,且指向相同方向:正指令会将天线移向右侧负指令则移向左侧。因此,要实现对称动作,需要对两个天线发出相反的指令:
    • 右侧天线:
      • 负值向外倾斜
      • 正值向内倾斜
    • 左侧天线:
      • 正值向外倾斜
      • 负值向内倾斜
  • 你可以通过 np.deg2rad(45)np.radians(45) 将度数转换为弧度。
  • 天线非常适合展现个性!不妨多试试,让 Reachy Mini 更具独特风格。

结合头部与天线运动

现在让我们通过组合两者来创造更复杂的表情!

with ReachyMini() as mini:
    # 好奇的表情:歪头 + 不对称的天线
    print("好奇...")
    mini.goto_target(
        head=create_head_pose(roll=20, degrees=True),
        antennas=[np.deg2rad(-60), np.deg2rad(-30)],
        duration=2.0,
    )

    # 回到中立位置
    mini.goto_target(head=create_head_pose(), antennas=[0.0, 0.0], duration=1.0)

    # 悲伤:低头 + 天线低垂
    print("悲伤...")
    mini.goto_target(
        head=create_head_pose(pitch=30, degrees=True),
        antennas=(np.deg2rad(-140), np.deg2rad(140)),
        duration=3.0,
    )

    # 回到中立位置
    mini.goto_target(head=create_head_pose(), antennas=[0.0, 0.0], duration=1.0)

    print("完成!")

组合运动的小贴士:

  • 你可以通过同步头部朝向与天线位置来创造富有表现力的行为。例如:

    • 悲伤表情:低头 + 天线向外低垂
    • 好奇表情:歪头 + 不对称天线
    • 兴奋表情:抬头 + 天线快速摆动
  • 时机把握很关键:对于情感表达,使用较长的持续时间(2-3 秒)能让动作更可信、更自然

  • 尝试不对称:不同的天线位置能带来更多的个性和特色

播放预录的情感

手动设计每一个姿势确实富有表现力,但 Reachy Mini 也自带了一个预录情感库——这些完整的动作序列结合了头部和天线的运动,可以表达喜悦、惊讶、无聊等多种情感。

这些情感数据存储在 HuggingFace 数据集 pollen-robotics/reachy-mini-emotions-library 中,可以直接通过 SDK 进行流式传输和播放。你可以在 Reachy Mini Emotion App 中浏览和预览所有情感。

下面让我们加载数据集并播放几个情感吧!

from reachy_mini.motion.recorded_move import RecordedMoves

EMOTIONS_DATASET = "pollen-robotics/reachy-mini-emotions-library"

emotions = RecordedMoves(EMOTIONS_DATASET)

print(f"共有 {len(emotions.list_moves())} 种情感可用:")
print(emotions.list_moves())

该应用程序中提供了 81 种情感,你也可以通过 SDK 控制头部和天线来创建自己的自定义情感。这是练习和发挥 Reachy Mini 表现力的好方法!

让我们播放一个应用程序中的随机情感,看看机器人的表现如何!每次运行,它都会从 81 种情感中随机选择一种,并在你的 Reachy Mini 上执行相应的头部和天线动作。这是探索机器人各种表情的趣味方式!

你也可以尝试在机器人上复现这些情感,并以此为灵感创作属于你自己的表情!

# 从数据集中随机选取一种情绪并进行播放

import asyncio
import random
from reachy_mini import ReachyMini
from reachy_mini.motion.recorded_move import RecordedMoves

EMOTIONS_DATASET = "pollen-robotics/reachy-mini-emotions-library"

async def play_random_emotion():
    # 1. 加载动作库
    emotions = RecordedMoves(EMOTIONS_DATASET)
    move_list = emotions.list_moves()

    # 2. 使用标准的 with (同步) 初始化机器人
    # 即使在异步函数内,如果对象本身不支持 async with,也要用 with
    with ReachyMini() as mini:
        emotion_name = random.choice(move_list)
        print(f"正在播放“{emotion_name}”...")
        
        # 3. 此时再调用异步执行方法
        await mini.async_play_move(emotions.get(emotion_name), initial_goto_duration=1.0)
        print("完成!")

if __name__ == "__main__":
    asyncio.run(play_random_emotion())

Notebook 1 — Basic Media: Camera & Audio

🎯 目标:学习拍摄图像、录制音频与播放声音 —— 让 Reachy 看得见、听得见!

⚠️ 注意:本笔记本需要摄像头和麦克风能够正常工作。这次请务必不要使用 media_backend="no_media"

相机基础知识 📸

拍摄你的第一张图像

让我们从 Reachy 的摄像头中捕获一帧图像并显示出来!

import cv2
from reachy_mini import ReachyMini

# 连接时启用媒体功能(注意,我们没有使用 media_backend="no_media")
with ReachyMini() as mini:
    print("正在捕获图像...")

    # 从摄像头获取一帧画面
    frame = mini.media.get_frame()
    # 可能需要稍等片刻,让摄像头完成初始化,因此可以循环等待,直到获得有效帧
    while frame is None:
        print("等待摄像头初始化...")
        time.sleep(0.5)
        frame = mini.media.get_frame()

    # 检查是否获得了有效帧
    if frame is not None:
        print("✓ 图像捕获成功!")
        print(f"  分辨率:{frame.shape[1]}x{frame.shape[0]}")
        print(f"  通道数:{frame.shape[2]}(BGR 格式)")
        print(f"  数据类型:{frame.dtype}")

        _, buffer = cv2.imencode(".jpg", frame)
        display(IPImage(data=buffer.tobytes()))
    else:
        print("✗ 图像捕获失败")
  • mini.media.get_frame():返回一个包含图像的 numpy 数组
  • 格式:OpenCV 格式(BGR 颜色顺序)
  • 形状:(高度, 宽度, 通道数)
  • 数据类型:uint8(每个像素的取值范围为 0-255)

⚠️ 重要提示:获取的帧为 BGR 格式(蓝-绿-红)。使用 IPython.display 保存或显示时,OpenCV 会自动处理编码。

将图像保存到磁盘

让我们把捕获的图像保存为文件。

import cv2
from reachy_mini import ReachyMini

with ReachyMini() as mini:
    frame = mini.media.get_frame()
    while frame is None:
        print("等待摄像头初始化...")
        time.sleep(0.5)
        frame = mini.media.get_frame()

    if frame is not None:
        # 使用 OpenCV 保存(无需转换——它本身就要求 BGR 格式)
        filename = f"reachy_photo_{int(time.time())}.jpg"
        cv2.imwrite(filename, frame)
        print(f"✓ 图像已保存为:{filename}")
    else:
        print("✗ 没有可保存的帧")

实时摄像头画面

你还可以使用 clear_output 在 notebook 中直接串流摄像头画面!

import cv2
from reachy_mini import ReachyMini
from IPython.display import clear_output

STREAM_DURATION = 10  # 秒

with ReachyMini() as mini:
    # 等待摄像头初始化
    frame = mini.media.get_frame()
    while frame is None:
        time.sleep(0.1)
        frame = mini.media.get_frame()

    start_time = time.time()
    while time.time() - start_time < STREAM_DURATION:
        frame = mini.media.get_frame()
        if frame is not None:
            _, buffer = cv2.imencode(".jpg", frame)
            clear_output(wait=True)
            display(IPImage(data=buffer.tobytes()))
        time.sleep(0.033)  # 约 30 fps

print("串流结束!")

音频基础 🎤🔊

了解 Reachy 的音频系统

Reachy Mini 配备:

  • 麦克风阵列:ReSpeaker 4 麦克风阵列
  • 扬声器:用于播放声音
  • 采样率:16 kHz(每秒 16,000 个采样点)
  • 格式:单声道或立体声音频,取决于具体操作

让我们检查一下音频配置:

from reachy_mini import ReachyMini

with ReachyMini() as mini:
    input_rate = mini.media.get_input_audio_samplerate()
    output_rate = mini.media.get_output_audio_samplerate()

    print(f"输入(麦克风)采样率:{input_rate} Hz")
    print(f"输出(扬声器)采样率:{output_rate} Hz")

录制音频

让我们用麦克风录制 3 秒音频。

import time
import numpy as np
from reachy_mini import ReachyMini

RECORD_DURATION = 3  # 秒

with ReachyMini() as mini:
    print(f"录制时长 {RECORD_DURATION} 秒...")
    print("🎤 说点什么或发出声音吧!")

    # 开始录制
    mini.media.start_recording()

    audio_samples = []
    sample_rate = mini.media.get_input_audio_samplerate()
    target_samples = int(RECORD_DURATION * sample_rate)  # 需要采集的总采样点数
    total_samples_collected = 0

    # 持续采集音频采样点,直到达到目标数量
    while total_samples_collected < target_samples:
        sample = mini.media.get_audio_sample()

        if sample is not None:
            audio_samples.append(sample)
            total_samples_collected += len(sample)
            current_duration = total_samples_collected / sample_rate
            print(f"\r录制中... {current_duration:.1f}秒", end="")
        else:
            # 仅在没有新采样点时休眠等待
            time.sleep(0.01)

    # 停止录制
    mini.media.stop_recording()

    print("\n✓ 录制完成!")
    print(f"  共捕获 {len(audio_samples)} 个采样块")

    # 拼接音频数据并按精确时长裁剪
    if audio_samples:
        audio_data = np.concatenate(audio_samples, axis=0)
        audio_data = audio_data[:target_samples]  # 裁剪至精确长度
        print(f"  音频数据总形状:{audio_data.shape}")
        print(f"  时长:{len(audio_data) / sample_rate:.2f} 秒")
    else:
        print("  ⚠ 未捕获到音频数据")
        audio_data = None
  • start_recording():启用麦克风音频采集
  • get_audio_sample():返回一个音频数据块(numpy 数组)
  • stop_recording():关闭音频采集
  • 数据格式:float32 类型的 numpy 数组
  • 采样块:每次调用 get_audio_sample() 会返回一个小数据块(通常相当于 0.1 到 0.2 秒的音频)

⚠️ 重要提示:我们跟踪的是采集到的音频采样点总数(而非实际耗时),以确保精确获取所请求时长的音频。随后我们会将最终音频数据裁剪至精确长度,避免录制时间比预期略长。

⚠️ 性能提示:为了实现最佳响应速度,我们仅在无采样点可用(sample 为 None)时休眠。这样就能以最快的速度处理传入的音频数据,这对于声音检测或监控等实时应用尤为重要。

你也可以直接在 notebook 中使用 IPython 的 Audio 组件播放录制好的音频——无需通过扬声器输出!

⚠️ 注意:Audio 组件仅在基于浏览器的 Jupyter 服务器(在终端中运行 jupyter notebookjupyter lab)中才会发出声音。在 VS Code 的 notebook 编辑器中,它能够正常显示,但会保持静音。

from IPython.display import Audio

if audio_data is not None:
    # Audio 组件需要单声道(1D)或通道优先(2, N)格式的数组。
    # ReSpeaker 返回的是(采样点数, 通道数)格式,因此我们取各通道的平均值。
    mono = audio_data.mean(axis=1) if audio_data.ndim > 1 else audio_data
    display(Audio(data=mono, rate=sample_rate))
else:
    print("尚未录制音频。请先运行录制单元格!")

将音频保存到文件

让我们把录制的音频保存为 WAV 文件。

if audio_data is not None and len(audio_data) > 0:
    filename = f"reachy_recording_{int(time.time())}.wav"
    sample_rate = 16000

    # 使用 soundfile 保存
    sf.write(filename, audio_data, sample_rate)
    print(f"✓ 音频已保存为:{filename}")
    print("  你可以用任何媒体播放器播放它!")
else:
    print("没有可保存的音频数据。请先运行录制单元格!")

播放音频

现在,让我们通过 Reachy 的扬声器来播放音频!

为此,我们将创建一个简单的辅助函数:

def play_audio_file(mini, audio_file_path):
    """通过 Reachy 的扬声器播放音频文件。

    参数:
        mini:ReachyMini 实例
        audio_file_path:WAV 文件的路径

    """
    # 加载音频文件
    data, _ = sf.read(audio_file_path, dtype="float32")

    # 开始播放
    mini.media.start_playing()
    print("🔊 正在播放音频...")

    # 以块的形式推送音频采样点
    chunk_size = 1024
    for i in range(0, len(data), chunk_size):
        chunk = data[i : i + chunk_size]
        mini.media.push_audio_sample(chunk)

    # 等待播放完成
    time.sleep(len(data) / mini.media.get_output_audio_samplerate())
    mini.media.stop_playing()
    print("✓ 播放完成!")

print("辅助函数已定义!")

它使用了 ReachyMini 的不同功能:

  • get_output_audio_samplerate():获取 ReachyMini 所需的采样率,以便在需要时进行重采样。
  • start_playing():启动音频输出
  • push_audio_sample():将音频数据推送到输出设备
  • stop_playing():停止音频输出

现在,让我们用新创建的函数来播放刚刚录制的音频!

import time
import soundfile as sf
from reachy_mini import ReachyMini

# 请确保先运行录制和保存单元格!
# 将以下文件名更新为你实际保存的录制文件
audio_file = "reachy_recording_1770979828.wav"  # 请更新为实际文件名

# 或者使用变量中最后保存的文件
if "filename" in dir():
    audio_file = filename
    print(f"正在使用文件:{audio_file}")

try:
    with ReachyMini() as mini:
        play_audio_file(mini, audio_file)
except FileNotFoundError:
    print(f"⚠ 文件未找到:{audio_file}")
    print("请确保先运行录制和保存单元格!")

生成并播放提示音

让我们通过编程来生成一个简单的蜂鸣声!

import time
import numpy as np
from reachy_mini import ReachyMini

def generate_beep(frequency=440, duration=0.5, sample_rate=16000):
    """生成一个简单的正弦波提示音。

    参数:
        frequency:频率(单位:Hz,440 = A 音)
        duration:持续时间(单位:秒)
        sample_rate:采样率(单位:Hz)

    返回:
        音频采样点的 numpy 数组

    """
    t = np.linspace(0, duration, int(sample_rate * duration))
    tone = 0.3 * np.sin(2 * np.pi * frequency * t)  # 0.3 = 音量

    # 添加淡入/淡出效果以避免咔嗒声
    fade_samples = int(sample_rate * 0.01)  # 10 毫秒的淡入淡出
    fade_in = np.linspace(0, 1, fade_samples)
    fade_out = np.linspace(1, 0, fade_samples)
    tone[:fade_samples] *= fade_in
    tone[-fade_samples:] *= fade_out

    return tone.astype(np.float32)


# 生成一个蜂鸣声
beep = generate_beep(frequency=880, duration=0.3)  # 高音 A

with ReachyMini() as mini:
    mini.media.start_playing()
    print("🔔 蜂鸣!")

    # 推送蜂鸣声数据
    chunk_size = 1024
    for i in range(0, len(beep), chunk_size):
        chunk = beep[i : i + chunk_size]
        mini.media.push_audio_sample(chunk)

    time.sleep(0.5)
    mini.media.stop_playing()
    print("完成!")

调整音量

你可以通过守护进程的 REST API 调整 Reachy Mini 的扬声器和麦克风音量。这在音频输出太响或太轻时非常有用。

⚠️ 注意:音量控制由守护进程管理,而非直接通过 Python SDK 进行。我们使用 requests 库来调用该 API。

守护进程的 URL 取决于你的设置:

import requests

# 根据你的设置调整此 URL:
# - Lite 版本:"http://localhost:8000"
# - 无线版本:"http://reachy-mini.local:8000"(或机器人的 IP 地址)
DAEMON_URL = "http://reachy-mini.local:8000"

# 获取当前扬声器音量
response = requests.get(f"{DAEMON_URL}/api/volume/current")
print(f"扬声器音量:{response.json()['volume']}%")

# 设置扬声器音量(0-100)
response = requests.post(f"{DAEMON_URL}/api/volume/set", json={"volume": 75})
print(f"扬声器音量已设置为:{response.json()['volume']}%")

# 你也可以控制麦克风输入音量:
response = requests.get(f"{DAEMON_URL}/api/volume/microphone/current")
print(f"麦克风音量:{response.json()['volume']}%")

可用的音量端点:

端点方法描述
/api/volume/currentGET获取当前扬声器音量(0-100)
/api/volume/setPOST设置扬声器音量({“volume”: 75})
/api/volume/microphone/currentGET获取当前麦克风音量
/api/volume/microphone/setPOST设置麦克风音量

你也可以通过 Reachy Mini Control 来调整音量。

⚠️ 提示:你还可以在 http://{守护进程地址}:8000/docs 交互式地浏览所有可用的 API 端点。

结合媒体与动作

让我们将所学内容融会贯通,让 Reachy 更具互动性!

示例:带倒计时的拍照环节

本示例创建了一个有趣的拍照流程,融合了以下功能:

  • 🎵 音频:用于倒计时和成功提示的蜂鸣声
  • 🤖 动作:头部定位与触角摆动
  • 📸 摄像头:拍摄最终照片

机器人将会:

  1. 移动至中立位置
  2. 通过蜂鸣声和触角动作从 3 倒数到 1
  3. 拍摄照片
  4. 播放成功提示音并执行庆祝动画
  5. 显示拍摄的照片
# “拍照”行为:蜂鸣、看向摄像头、拍照、再次蜂鸣
with ReachyMini() as mini:
    print("📸 拍照环节开始...")

    # 移至中立位置
    mini.goto_target(head=create_head_pose(), antennas=[0.0, 0.0], duration=1.0)

    # 伴随蜂鸣声与头部动作进行倒计时
    for i in [3, 2, 1]:
        print(f"  {i}...")

        # 蜂鸣
        beep = generate_beep(frequency=440 + i * 100, duration=0.2)
        mini.media.start_playing()
        for j in range(0, len(beep), 1024):
            mini.media.push_audio_sample(beep[j : j + 1024])

        # 摆动触角
        mini.goto_target(antennas=[0.3, -0.3], duration=0.3)
        mini.goto_target(antennas=[0.0, 0.0], duration=0.3)

        time.sleep(0.5)

    # 拍照!
    print("  📸 咔嚓!")
    frame = mini.media.get_frame()

    # 成功提示音(音调更高)
    success_beep = generate_beep(frequency=840, duration=0.4)
    mini.media.start_playing()
    for j in range(0, len(success_beep), 1024):
        mini.media.push_audio_sample(success_beep[j : j + 1024])
    time.sleep(0.5)
    # mini.media.stop_playing()

    # 庆祝动画
    mini.goto_target(
        head=create_head_pose(pitch=-10, degrees=True),
        antennas=[0.5, -0.5],
        duration=0.5,
    )

    # 保存并显示
    if frame is not None:
        filename = f"reachy_selfie_{int(time.time())}.jpg"
        cv2.imwrite(filename, frame)
        print(f"\n✓ 照片已保存:{filename}")

        # 显示
        _, buffer = cv2.imencode(".jpg", frame)
        display(IPImage(data=buffer.tobytes()))

    # 返回中立位置
    mini.goto_target(head=create_head_pose(), antennas=[0.0, 0.0], duration=1.0)

参考资料

相关文章

🤖

智能问答助手

⏳ 初始化...

💡 配置和聊天记录仅保存在本地浏览器中