填坑!在五一之前将LErobot语音控制部分开源,附带代码解读

Lerobot--so100,不仅能用来强化学习更能通过大模型控制!!

 

Lerobot--so100,不仅能用来强化学习更能通过大模型控制!!_lerobot 逆解-CSDN博客

        这是之前发布的一篇文章,当时答应在优化后开源,但是最近一直忙着其他事,没时间优化,但是五一之前,也先填坑吧,后续有其他改动,在进行补充。

lerobot_keycon_gposAddllm111.py;;;这是仓库中代码的名称;

import os
import mujoco
import mujoco.viewer
import numpy as np
import time
from lerobot_kinematics import lerobot_IK, lerobot_FK, get_robot
import rospy
from std_msgs.msg import String
import threading

  • os:用于处理文件路径和环境变量。
  • mujoco 和 mujoco.viewer:MuJoCo 物理仿真引擎及其可视化工具。
  • numpy:用于数值计算。
  • time:用于时间管理。
  • lerobot_kinematics:自定义模块,包含逆运动学(lerobot_IK)、正运动学(lerobot_FK)函数和获取机器人模型的函数(get_robot)。(这个呢是盒子桥作的工作,整体仓库基本也是他所作的工作,但是为了验证想法,在其中作了一点点改动)
  • rospy 和 std_msgs.msg.String:ROS 相关库,用于节点初始化、消息订阅。
  • threading:用于多线程编程,保证线程安全。这个还是有必要的,前期没有进行多线程的话就会出现报错,以及响应差。

np.set_printoptions(linewidth=200)
os.environ["MUJOCO_GL"] = "glfw"
JOINT_NAMES = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"]
current_script_path = os.path.dirname(os.path.abspath(__file__))
xml_path = os.path.join(current_script_path, "scene.xml")
mjmodel = mujoco.MjModel.from_xml_path(xml_path)
qpos_indices = np.array([mjmodel.jnt_qposadr[mjmodel.joint(name).id] for name in JOINT_NAMES])
mjdata = mujoco.MjData(mjmodel)
JOINT_INCREMENT = 0.005
POSITION_INSERMENT = 0.0008
robot = get_robot('so100')
control_qlimit = [[-2.1, -3.1, -0.0, -1.375,  -1.57, -0.15], 
                  [ 2.1,  0.0,  3.1,  1.475,   3.1,  1.5]]
control_glimit = [[0.125, -0.4,  0.046, -3.1, -0.75, -1.5], 
                  [0.340,  0.4,  0.23, 2.0,  1.57,  1.5]]
init_qpos = np.array([0.0, -3.14, 3.14, 0.0, -1.57, -0.157])
target_qpos = init_qpos.copy()
init_gpos = lerobot_FK(init_qpos[1:5], robot=robot)
target_gpos = init_gpos.copy()
lock = threading.Lock()
  • np.set_printoptions(linewidth=200):设置numpy数组打印的行宽。
  • os.environ["MUJOCO_GL"] = "glfw":指定 MuJoCo 使用glfw作为渲染后端。
  • JOINT_NAMES:定义机器人关节名称。
  • xml_path:获取 MuJoCo 模型的 XML 文件路径。这些你们看着修改就行
  • mjmodel 和 mjdata:加载 MuJoCo 模型和数据。
  • JOINT_INCREMENT 和 POSITION_INSERMENT:分别为关节角度和位置的增量。
  • control_qlimit 和 control_glimit:关节角度和位置的限制范围。
  • init_qpos 和 init_gpos:初始关节角度和位置。
  • target_qpos 和 target_gpos:目标关节角度和位置。
  • lock:线程锁,用于保证线程安全。

keys_received = {}

def ros_message_callback(msg):
    key = msg.data
    if key in key_to_joint_increase:
        with lock:
            keys_received[key] = 1  # Increase direction
    elif key in key_to_joint_decrease:
        with lock:
            keys_received[key] = -1  # Decrease direction
    elif key == "0":
        with lock:
            global target_qpos, target_gpos
            target_qpos = init_qpos.copy()  # Reset to initial position
            target_gpos = init_gpos.copy()  # Reset to initial gripper position
  • key_to_joint_increase 和 key_to_joint_decrease:定义按键与关节控制的映射关系。
  • keys_received:用于记录当前接收到的按键及其方向。
  • ros_message_callback:ROS 消息回调函数,依据接收到的按键消息更新keys_received字典,若收到"0"则将机器人重置到初始位置。

这里比较巧妙的用ros参与 ,接收从LLM发送过来的话题,然后将话题的信息给回调函数,进行控制。

然后就是关于:voiceLLMcom.py的代码部分::

import openai
from httpx_socks import SyncProxyTransport
import httpx
import os
import rospy
from std_msgs.msg import String
import sounddevice as sd
from scipy.io.wavfile import write
import speech_recognition as sr
from pynput import keyboard
import numpy as np

  • openai:用于调用百炼(这里使用了兼容 OpenAI 接口的形式)的 API 来处理自然语言指令。
  • SyncProxyTransport 和 httpxSyncProxyTransport 用于设置代理,httpx 是一个 HTTP 客户端库,用于发送 HTTP 请求。
  • os:用于处理文件路径和环境变量等操作系统相关操作。
  • rospy 和 std_msgs.msg.String:ROS(机器人操作系统)相关库,rospy 用于初始化 ROS 节点、发布消息等操作,String 是 ROS 中消息类型的一种。
  • sounddevice:用于录制音频。
  • scipy.io.wavfile 中的 write:用于将录制的音频数据保存为 WAV 文件格式。
  • speech_recognition:用于语音识别,将录制的音频转换为文本。
  • pynput 中的 keyboard:用于监听键盘按键事件,以控制录音的开始和停止。
  • numpy:用于数值计算和处理音频数据。      ---------
transport = SyncProxyTransport.from_url('
client = httpx.Client(transport=transport)

openai_client = openai.OpenAI(
    api_key=
    base_url="
    http_client=client
)

相关的api之类的需要你们自己去配一下,我使用的是通义千问,相关的你们可以在官网找到。

  • 首先设置了一个代理,这里使用了 SOCKS5 代理,地址为 127.x.x.x
  • 创建了一个 httpx 客户端,并将代理传输设置给它。
  • 初始化 openai 客户端,使用环境变量 DASHSCOPE_API_KEY 作为 API 密钥,设置 API 的基础 URL 为百炼的兼容模式 URL,并使用之前创建的 httpx 客户端来发送请求。

设计提示词:

prompt_template = """
你是一个非常专业的机械臂控制工程师,负责将自然语言指令转换为机械臂控制按键。并且你的输出结果只有纯粹的字母,没有任何多余的文字
按键映射如下:
- 'w': 机械臂向前移动
- 'a': 机械臂向右移动
- 'r': 机械臂向上移动
- 'q': 机械臂夹子向左转
- 'g': 机械臂俯仰增加
- 'z': 夹爪打开
- 's': 机械臂向后移动
- 'd': 机械臂向左移动
- 'f': 机械臂向下移动
- 'e': 机械臂夹子向右转
- 't': 机械臂俯仰减少
- 'c': 夹爪关闭

请根据以下自然语言指令输出对应的按键:
{input}
"""

定义了一个提示模板,用于向百炼模型提供指令,使其能够根据给定的自然语言指令,按照机械臂按键映射关系输出对应的按键字母。

调用百炼模型将自然语言转换为按键

def get_key_from_model(question):
    prompt = prompt_template.format(input=question)
    try:
        response = openai_client.chat.completions.create(
            model="qwen-plus",
            messages=[
                {'role': 'system', 'content': 'You are a helpful assistant.'},
                {'role': 'user', 'content': prompt}
            ]
        )
        key = response.choices[0].message.content.strip()
        remove_texts = [
            "根据指令“打开夹子”对应的按键映射是:",
            "根据指令“闭合夹子”,对应的按键是:",
            "根据按键映射,打开夹爪对应的按键是:",
            "因此指令转换结果为:"
        ]
        for text in remove_texts:
            if key.startswith(text):
                key = key.replace(text, "").strip()
        if key.startswith("`") and key.endswith("`"):
            key = key[1:-1].strip()
        key = ''.join(filter(str.isalpha, key))
        return key
    except Exception as e:
        print(f"百炼API调用失败:{str(e)}")
        return None
  • 函数 get_key_from_model 接受一个自然语言问题作为参数,将问题填充到提示模板中。
  • 调用百炼模型(模型名称为 qwen-plus),发送系统角色消息和用户角色消息(包含填充后的提示)。
  • 从模型的响应中提取出按键信息,并对提取的按键信息进行处理,去除一些可能存在的前缀文本和多余的字符,只保留字母。
  • 如果 API 调用失败,捕获异常并打印错误信息,返回 None
  • 录制音频
def record_audio():
    print("开始录音... 按 'q' 键停止")
    frames = []
    recording = True

    def on_press(key):
        nonlocal recording
        if key.char == 'q':
            recording = False

    listener = keyboard.Listener(on_press=on_press)
    listener.start()

    try:
        with sd.InputStream(
                samplerate=44100,
                channels=1,
                dtype=np.int16,
                blocksize=1024
        ) as stream:
            while recording:
                data, _ = stream.read(1024)
                frames.append(data)
    except KeyboardInterrupt:
        recording = False

    listener.stop()

    if frames:
        data = np.concatenate(frames, axis=0)
        os.makedirs(os.path.dirname("/home/zzq/voice_qwen/outputs/temp_audio.wav"), exist_ok=True)
        write("/home/zzq/voice_qwen/outputs/temp_audio.wav", 44100, data)
        print(f"录音保存为:/home/zzq/voice_qwen/outputs/temp_audio.wav")
        return "/home/zzq/voice_qwen/outputs/temp_audio.wav"
    else:
        print("未录制到音频数据")
        return None
  • 函数 record_audio 用于录制音频。首先打印提示信息,初始化一个空列表 frames 用于存储音频数据帧,设置 recording 标志为 True
  • 定义一个内部函数 on_press,当按下 q 键时,将 recording 标志设置为 False
  • 使用 keyboard.Listener 监听键盘按键事件,并启动监听器。
  • 使用 sounddevice.InputStream 创建一个音频输入流,设置采样率、通道数、数据类型和块大小。这里面的通道采样率数据类型等,需要你们根据你们自己的电脑调整
  • 在 recording 为 True 的情况下,不断从音频流中读取数据帧并添加到 frames 列表中。
  • 如果捕获到 KeyboardInterrupt 异常,将 recording 设置为 False
  • 停止监听器后,将所有音频数据帧连接成一个数组,并将音频数据保存为 WAV 文件,返回保存的音频文件路径。如果没有录制到音频数据,返回 None

语音识别:

def recognize_speech(audio_file):
    recognizer = sr.Recognizer()
    with sr.AudioFile(audio_file) as source:
        audio_data = recognizer.record(source)
    try:
        text = recognizer.recognize_google(audio_data, language="zh-CN")
        print("语音识别结果:", text)
        return text
    except sr.UnknownValueError:
        print("语音识别失败:无法识别音频内容")
        return None
    except sr.RequestError as e:
        print(f"语音识别失败:{e}")
        return None
  • 函数 recognize_speech 接受一个音频文件路径作为参数。
  • 使用 speech_recognition.Recognizer 创建一个语音识别器对象。
  • 使用 sr.AudioFile 打开音频文件,并通过识别器的 record 方法将音频数据读取为 AudioData 对象。
  • 尝试使用 recognize_google 方法将音频数据转换为文本,设置语言为中文(zh-CN)。如果识别成功,打印识别结果并返回文本;如果识别失败,捕获相应的异常并打印错误信息,返回 None
  • 主函数
  • def main():
        rospy.init_node('robot_key_publisher', anonymous=True)
        pub = rospy.Publisher('robot_key_command', String, queue_size=10)
        rate = rospy.Rate(1)  # 1 Hz
    
        while not rospy.is_shutdown():
            print("请开始语音输入,按 'q' 键结束录音(输入'exit'可退出程序)...")
            audio_file = record_audio()
            if not audio_file:
                continue
    
            user_input = recognize_speech(audio_file)
            if user_input:
                if user_input.lower() == 'exit':
                    print("接收到退出指令,程序即将退出...")
                    break
                print(f"\n你输入的指令:{user_input}")
                key = get_key_from_model(user_input)
    
                if key:
                    pub.publish(key)
                    print(f"已发布按键: {key}")
                else:
                    print("未能从模型获取有效按键")
            else:
                print("未成功识别语音内容,请重试。")
    
            rate.sleep()

  • 函数 main 是程序的入口点。首先初始化 ROS 节点,名称为 robot_key_publisher,设置 anonymous=True 以确保节点名称唯一。
  • 创建一个 ROS 发布者,发布到 robot_key_command 话题,消息类型为 String,队列大小为 10。
  • 设置循环的频率为 1Hz。
  • 在 ROS 节点未关闭的情况下,循环执行以下操作:
    • 提示用户开始语音输入,按 q 键结束录音,并说明输入 exit 可退出程序。
    • 调用 record_audio 录制音频,若未成功录制,跳过本次循环。
    • 调用 recognize_speech 对录制的音频进行语音识别,若识别成功:
      • 如果识别的文本为 exit,打印退出信息并跳出循环。
      • 否则,打印用户输入的指令,调用 get_key_from_model 将自然语言指令转换为按键。
      • 如果获取到有效按键,通过 ROS 发布者发布按键消息,并打印已发布的按键信息;否则,打印未能获取有效按键的信息。
    • 如果语音识别未成功,打印提示信息。
    • 按照设置的频率休眠。

最后,如何启动在仓库中的readme中有

https://github.com/box2ai-robotics/lerobot-kinematics?tab=readme-ov-file   ---这个是盒子桥大佬的github地址,先根据他的配置将环境搭建好,然后去我的仓库中将代码放进去,就ok

https://github.com/NANBEI132/lerobot_kinematics_LLM      ---这个是放我的两个代码的地方

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值