Skip to content
donghee-aiPublic

About

말로 지시하면 SO-101 양팔 로봇이 쓰레기를 모으는 Physical AI 데모 — Vision(YOLO) · Language(LLM 의도 게이트) · Action(LeRobot ACT) 통합

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

59 Commits

Folders and files

Repository files navigation

PAI — 말로 지시하는 쓰레기 수거 로봇

자연어로 지시하면 SO-101 양팔 로봇이 책상 위 쓰레기를 쓸어 담는 Physical AI 데모. Vision(카메라·YOLO) · Language(LLM·의도 게이트) · Action(LeRobot ACT 정책)을 하나의 파이프라인으로 통합했다.

데모 — "청소해줘" 입력 후 양팔이 쓰레기를 쓰레받기에 수거

위 영상: PAI-Language UI 에 청소해줘 입력 → 5557 트리거 발행 → ACT 정책이 양팔을 구동해 초록 조각 3개를 쓰레받기 안으로 수거. 원본 화질: docs/media/demo.mp4 (Jetson AGX Thor + SO-101 양팔, 실기 촬영)

역할 — Language 파트(자연어 이해·의도 게이트), LeRobot 연동 어댑터(rollout_with_zmq_task), 3창 통합과 오케스트레이션을 맡았다. Vision 파트는 팀원 @yeounhyeok 의 원저작이며 git subtree 로 히스토리째 합쳤다.


어떻게 도는가

 PAI-Vision ──ZMQ:5555 raw 프레임(front/wrist)──▶ LeRobot (ACT 롤아웃, ZMQCamera)
      └───────WS:8000 장면 JSON────────────────▶ PAI-Language (LLM)
 PAI-Language ──ZMQ:5557 instruction──────────▶ LeRobot (rollout_with_zmq_task 어댑터)
채널 포트 bind 주체 소비 주체
raw 프레임 (ZMQ) 5555 Vision LeRobot ZMQCamera (camera_name=front/wrist)
장면 JSON (WS) 8000 Vision Language
instruction (ZMQ) 5557 Language rollout_with_zmq_task

카메라는 Vision 이 단독 소유한다. 같은 USB 카메라를 두 프로세스가 동시에 열 수 없어서, 로봇 쪽은 /dev/video* 를 직접 열지 않고 Vision 이 중계하는 ZMQ 프레임을 관측으로 받는다. 프레임 발행기는 Vision 파트에 있다(팀원 원저작).

설계 판단 ① — 명령 한 번에 정해진 시간만 움직인다 (원샷 게이팅)

LeRobot 롤아웃은 시작하면 끝까지 계속 돈다. 이 데모는 "말했을 때 그 한 번만" 움직여야 하므로, 롤아웃을 고치는 대신 get_action 을 감싸는 어댑터를 만들었다 (rollout_with_zmq_task.py). 실행 창 밖에서는 정책 출력 대신 캡처해둔 초기 자세를 계속 명령해 팔을 그 포즈에 고정한다. 트리거 수신 → 2초 대기(즉시 움직이면 팔이 튄다) → N초 구동 → 복귀. "멈춰" 는 창을 즉시 닫는다.

여기서 스레드 경계가 걸린다. instruction 은 ZMQ SUB 스레드로 들어오는데 get_action 은 제어 루프 메인 스레드에서 불린다. 트리거마다 엔진을 reset 해야 하는데 (→ 겪은 문제와 원인) SUB 스레드에서 engine.reset() 을 직접 부르면 추론 중인 상태를 건드린다. 락 대신 reset_pending 플래그로 풀었다 — SUB 는 set 만, 메인이 실행 창 첫 스텝에서 consume. 쓰는 쪽과 지우는 쪽이 하나씩이라 경합이 성립하지 않는다.

설계 판단 ② — Language 가 의미 게이트를 맡는다

정책 act-trash-gathering 은 ACT(언어 비조건부) 다. instruction 의 내용 으로 행동이 바뀌지 않고, 학습된 단일 태스크(쓰레기 모으기)만 수행한다. 그래서 "말대로 조종"이 성립하지 않는다.

대신 Language 가 의도를 판별해 그때만 로봇을 트리거한다.

입력 동작
"쓰레기 좀 모아줘" · "책상 정리해줘" · "clean up" 트리거 발행 → EXEC_SECONDS 동안 구동 후 정지
"마우스 집어줘" · "안녕?" 대화 응답만, 로봇 미실행
"멈춰" STOP sentinel 발행 → 즉시 정지 + ACT 큐 비우기

판별 키워드는 language/.env 의 TRASH_KEYWORDS 로 조정한다. 게이트 구현: language/language/main.py 의 _is_trash_intent.

겪은 문제와 원인

실기로 돌리며 만난 것 중, 증상만 보고는 원인을 못 맞힌 것들.

증상 실제 원인 대응
같은 명령인데 1·2회차 동작이 다름 ACT 내부 액션 청크 큐·temporal-ensemble 버퍼가 idle 동안 얼어 있다가 잔여분부터 재생 트리거마다 engine.reset(). SUB↔메인 스레드 레이스는 reset_pending 플래그로 회피
개선했더니 동작이 위험하게 빨라짐 --fps 는 상한만 지정 — 이전 CPU 경로가 25Hz 를 못 채우고 있었을 뿐 --fps 인자 노출 + 기본 20Hz
미리보기 창이 2개 뜸 카메라 워커마다 cv2.imshow show_window — 첫 카메라만 창, 나머지는 스트림만

첫 항목의 reset 은 소프트웨어 잔여만 없앤다. ACT 는 현재 팔 자세와 화면으로 궤적을 재현하므로, 트리거 사이에 팔이 초기 자세로 돌아가지 않거나 쓰레기 배치가 달라지면 동작은 여전히 달라진다. 이건 정책의 성질이라 코드로 지울 수 없다.

두 번째는 방향이 반대다 — 빨라진 것이 문제를 만들었다. 느린 CPU 경로가 우연히 속도 제한기 노릇을 하고 있었을 뿐이고, 목표 25Hz 를 꽉 채우자 오히려 학습 분포와 어긋났다.

이 외에 컨테이너 venv 가 uv 기반이라 pip 이 없던 문제, docker commit 이 entrypoint 를 삼켜 이미지가 깨진 사고, 매 실행 44MB 를 재다운로드하던 torch 캐시 등 8건의 기록: docs/history/

구성

디렉터리 역할
vision/ 카메라 단독 소유 → YOLO 장면(WS:8000) + raw 프레임(ZMQ:5555) 송출
language/ 자연어 → OpenAI → 대화 응답 + 의도 게이트(ZMQ:5557). LeRobot 어댑터 scripts/rollout_with_zmq_task.py 포함
orchestration/ 실행 스크립트, Docker(lerobot-workspace), 캘리브레이션, udev 포트 고정, 정책 설정
docs/ 런북 · 트러블슈팅 기록 · 데모 미디어

실행

준비 (한 번만)

# 1) venv 2개
cd vision   && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt && cd ..
cd language && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt && cd ..

# 2) .env (키는 저장소에 없다)
cp vision/.env.example   vision/.env      # CAMERAS=front:2,wrist:0, ZMQ_PUBLISH_ENABLED=true
cp language/.env.example language/.env    # ★ OPENAI_API_KEY, INSTRUCTION_PUB_ENABLED=1

# 3) 정책 가중치 (198MB — 저장소에 없다). 공개 저장소라 토큰·추가 패키지 불필요
bash orchestration/scripts/fetch-model.sh

# 4) 하드웨어 포트 고정 (팔 serial / 카메라 USB 포트 기준 심볼릭 링크)
sudo bash orchestration/scripts/install-udev.sh

# 5) Docker 이미지
sudo docker compose -f orchestration/docker/docker-compose.yml build

3창 실행

xhost +local:
orchestration/scripts/pai-run-all.sh 25      # 명령 1건당 구동 25초

창1(Vision) → 창2(Language UI) → 창3(LeRobot) 순서로 뜬다. 창2에 "쓰레기 좀 모아줘" 를 입력하면 로봇이 움직인다.

수동 실행과 단계별 격리 검증은 런북 참고.

검증

하드웨어 없이 파이프라인 뼈대를 확인할 수 있다. 로봇을 붙이기 전에 이걸로 채널부터 격리해 두면 실패가 소프트웨어인지 하드웨어인지 바로 갈린다.

language/.venv/bin/python -m pytest language/               # 유닛테스트
language/.venv/bin/python orchestration/scripts/verify-dryrun.py   # 의도 게이트 + 5557 채널 + STOP
항목 상태
Language 유닛테스트 113 passed
의도 게이트 (트리거 4종 / 차단 4종) 통과
Language → ZMQ:5557 → 롤아웃 어댑터 채널 통과 (dry-run)
STOP sentinel 분기 통과
하드웨어 E2E (docker · ZMQ:5555 프레임 · 실제 구동) 위 데모 영상으로 입증

하드웨어 · 환경

  • 호스트: Jetson AGX Thor (aarch64, CUDA 13 / sm_110), Ubuntu 24.04, Python 3.12
  • 로봇: SO-ARM101 양팔 (follower 2 + leader 2), CH340 USB-serial
  • 카메라: 동일 모델 UVC 웹캠 2대 (front / wrist) — serial 이 같아 USB 포트 경로로 구분
  • LeRobot: v0.5.2 고정 (bd9619df)

⚠️ udev 규칙의 팔 serial 과 카메라 USB 포트 경로는 이 하드웨어 고유다. 다른 장비에서는 orchestration/docker/udev/ 아래 규칙을 자기 값으로 교체해야 한다.

외부 자산

종류 링크
ACT 정책 danny1002/act-trash-gathering
데이터셋 danny1002/trash_gathering (Apache-2.0)
LeRobot huggingface/lerobot v0.5.2

출처

이 저장소는 세 갈래로 개발되던 작업을 git subtree 로 히스토리째 합친 것이다.

파트 원본 저장소
vision/ yeounhyeok/PAI-Vision — 원저작
language/ donghee-ai/PAI-Language
orchestration/ hsu-pai/PAI-setup (+ so_arm101_setup 고유 자산)
흡수된 히스토리를 조회하는 법

git subtree add 로 합친 커밋들은 흡수 당시의 원래 경로를 유지한다. 따라서 git log -- vision/… 로는 조회되지 않고, 원본 경로나 흡수 지점 커밋을 기준으로 봐야 한다.

git log --oneline --graph --all                      # 전체 그래프 (병합 지점이 보인다)
git log --full-history -- app/adapters/run_all.py    # 원본 경로로 Vision 이력
git log --oneline f7f6275                            # 흡수된 Vision tip 에서 거슬러 올라가기

About

말로 지시하면 SO-101 양팔 로봇이 쓰레기를 모으는 Physical AI 데모 — Vision(YOLO) · Language(LLM 의도 게이트) · Action(LeRobot ACT) 통합

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages