diff --git a/frontend/src/pages/Recording.tsx b/frontend/src/pages/Recording.tsx index 840da851..479e5be7 100644 --- a/frontend/src/pages/Recording.tsx +++ b/frontend/src/pages/Recording.tsx @@ -72,6 +72,8 @@ interface BackendStatus { exit_early: boolean; rerecord_episode: boolean; }; + error?: string | null; + hint?: string | null; } const Recording = () => { @@ -88,6 +90,7 @@ const Recording = () => { null ); const [recordingSessionStarted, setRecordingSessionStarted] = useState(false); + const [sessionError, setSessionError] = useState<{ error: string; hint: string | null } | null>(null); const [optimisticPhase, setOptimisticPhase] = useState(null); const [showStopConfirm, setShowStopConfirm] = useState(false); @@ -141,7 +144,7 @@ const Recording = () => { // Poll backend status continuously to stay in sync useEffect(() => { - if (!recordingSessionStarted) return; + if (!recordingSessionStarted || sessionError !== null) return; const pollStatus = async () => { try { @@ -150,6 +153,15 @@ const Recording = () => { ); if (!response.ok) return; const status = await response.json(); + + if (status.current_phase === "error" && status.session_ended) { + setSessionError({ + error: status.error || "Recording session failed.", + hint: status.hint || null, + }); + return; + } + setBackendStatus(status); const currentOptimistic = optimisticPhaseRef.current; @@ -205,7 +217,7 @@ const Recording = () => { pollStatus(); const statusInterval = setInterval(pollStatus, 1000); return () => clearInterval(statusInterval); - }, [recordingSessionStarted, recordingConfig, navigate, baseUrl, fetchWithHeaders]); + }, [recordingSessionStarted, recordingConfig, navigate, baseUrl, fetchWithHeaders, sessionError]); const formatTime = (seconds: number): string => { const mins = Math.floor(seconds / 60); @@ -597,6 +609,39 @@ const Recording = () => { + + + + + + + Recording Session Failed + + +

The recording session encountered an error and had to stop.

+ +
+ {sessionError?.error} +
+ + {sessionError?.hint && ( +
+ Hardware Hint: + {sessionError.hint} +
+ )} +
+
+ + navigate("/")} + className="bg-red-600 hover:bg-red-700 text-white" + > + Return to Home + + +
+
); }; diff --git a/lelab/record.py b/lelab/record.py index f3a54b09..ebf20679 100644 --- a/lelab/record.py +++ b/lelab/record.py @@ -31,7 +31,7 @@ from lerobot.teleoperators.so_leader import SO101LeaderConfig from .utils.config import setup_calibration_files, with_lelab_tag -from .utils.devices import safe_disconnect_device +from .utils.devices import friendly_hint, safe_disconnect_device logger = logging.getLogger(__name__) @@ -409,10 +409,19 @@ def handle_recording_status() -> dict[str, Any]: logger.info("๐Ÿ“ก RECORDING STATUS REQUEST: Session has ended - frontend should stop polling") print("๐Ÿ“ก STATUS CHANGE: Frontend is still polling after session end - should stop now") + error = None + hint = None + if current_phase == "error" and last_recording_info is not None: + error = last_recording_info.get("error") + if error: + hint = friendly_hint(error) + status = { "recording_active": recording_active, "current_phase": current_phase, # "preparing", "recording", "resetting", "completed" "session_ended": session_ended, # New field to indicate session completion + "error": error, + "hint": hint, "available_controls": { "stop_recording": recording_active, # ESC key replacement "exit_early": recording_active, # Right arrow key replacement diff --git a/lelab/rollout.py b/lelab/rollout.py index cfff79a8..cef47fe9 100644 --- a/lelab/rollout.py +++ b/lelab/rollout.py @@ -37,6 +37,7 @@ from pydantic import BaseModel from .utils.config import setup_follower_calibration_file +from .utils.devices import friendly_hint logger = logging.getLogger(__name__) @@ -185,34 +186,6 @@ def _extract_error_from_log(log_path: str | None) -> str | None: return snippet or None -def _friendly_hint(error_text: str | None) -> str | None: - """A plain-language, actionable headline for the common SO-101 failures.""" - if not error_text: - return None - low = error_text.lower() - if "overload" in low or "torque_enable" in low: - return ( - "A motor overloaded โ€” usually the gripper holding an object too hard. Release the object / " - "open the gripper and power-cycle the arm before trying again." - ) - if "missing motor ids" in low or "motor check failed" in low: - return ( - "A follower motor isn't responding (often the gripper, id 6). If a skill was holding an object " - "it likely overloaded โ€” remove it, power-cycle the arm, then try teleoperation first." - ) - if "could not connect" in low or "failed to connect" in low or "not connected" in low: - return "Couldn't connect to the arm โ€” make sure it's plugged in, powered on, and on the right port." - if "frame is too old" in low or "no frame" in low or "frame timeout" in low: - return ( - "A camera can't keep up โ€” frames are arriving too slowly. Lower its resolution/FPS, " - "set FOURCC=MJPG, and close other heavy apps, then try again." - ) - if "failed to set capture_" in low or "actual_width" in low or "actual_height" in low: - return "A camera doesn't support the configured resolution โ€” open camera settings and click Auto." - if "permission" in low and ("port" in low or "com" in low): - return "Couldn't open the serial port โ€” close anything else using it, or run `lelab --stop`." - return None - # Errors that mean the policy actually ran and only shutdown/cleanup tripped โ€” # e.g. disabling torque on a gripper still holding an object. Connection-loss @@ -400,7 +373,7 @@ def handle_inference_status() -> dict[str, Any]: "exit_code": rc, "outcome": outcome, "error": error, - "hint": _friendly_hint(error), + "hint": friendly_hint(error), "policy_ref": finished_meta.get("policy_ref"), "duration_s": finished_meta.get("duration_s"), "log_path": finished_meta.get("log_path"), diff --git a/lelab/server.py b/lelab/server.py index 519e0c72..9a0bc470 100644 --- a/lelab/server.py +++ b/lelab/server.py @@ -1019,9 +1019,55 @@ def _v4l2_camera_name(index: int) -> str | None: return None -def _linux_cameras() -> list[dict[str, Any]]: +def _chrome_camera_name(index: int) -> str | None: + import os + import re + + # ๊ธฐ๋ณธ v4l2_name ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + v4l2_name = _v4l2_camera_name(index) + + try: + sys_path = f"/sys/class/video4linux/video{index}" + current_dir = os.path.join(sys_path, "device") + + for _ in range(5): + vid_path = os.path.join(current_dir, "idVendor") + pid_path = os.path.join(current_dir, "idProduct") + product_path = os.path.join(current_dir, "product") + + # vender id ์™€ product id๊ฐ€ ์žˆ๋‹ค๋ฉด + if os.path.exists(vid_path) and os.path.exists(pid_path): + try: + with open(vid_path, "r", encoding="utf-8") as f: + vid = f.read().strip() + with open(pid_path, "r", encoding="utf-8") as f: + pid = f.read().strip() + + if os.path.exists(product_path): + with open(product_path, "r", encoding="utf-8") as f: + base_name = f.read().strip() + else: + base_name = v4l2_name.split(":")[0].strip() + + # ํฌ๋กฌ ๋ธŒ๋ผ์šฐ์ € ํฌ๋งท์œผ๋กœ ์ด๋ฆ„ ์กฐํ•ฉ + base_name = base_name.replace("_", " ") + base_name = re.sub(r'\s+HD$', '', base_name).strip() + return f"{base_name} ({vid}:{pid})" + except Exception: + return v4l2_name + # ์ฐพ๋Š”๊ฒŒ ์—†์œผ๋ฉด ์ƒ์œ„ ํด๋”๋กœ ์ด๋™ + current_dir = os.path.join(current_dir, "..") + + # 5๋‹จ๊ณ„๋ฅผ ๊ฑฐ์Šฌ๋Ÿฌ ์˜ฌ๋ผ๊ฐ€๋„ ๋ชป ์ฐพ์œผ๋ฉด ์›๋ž˜ ์ด๋ฆ„ ๋ฐ˜ํ™˜ + return v4l2_name + except OSError: + return None + +def _linux_cameras(is_chrome: bool = False) -> list[dict[str, Any]]: """Enumerate Linux cameras, naming each from sysfs (no extra deps).""" import cv2 + import os + import re cameras: list[dict[str, Any]] = [] for i in range(10): @@ -1030,12 +1076,16 @@ def _linux_cameras() -> list[dict[str, Any]]: cap.release() if not opened: continue - cameras.append({"index": i, "name": _v4l2_camera_name(i) or f"Camera {i}", "available": True}) + + if is_chrome: + cameras.append({"index": i, "name": _chrome_camera_name(i) or f"Camera {i}", "available": True}) + else: + cameras.append({"index": i, "name": _v4l2_camera_name(i) or f"Camera {i}", "available": True}) return cameras @app.get("/available-cameras") -def get_available_cameras(): +def get_available_cameras(request: Request): """List cameras with the same index ordering cv2 will use to record. Each platform enumerates in the order its cv2 backend indexes devices, and @@ -1051,6 +1101,10 @@ def get_available_cameras(): import platform system = platform.system() + + # User-Agent๋ฅผ ํ™•์ธํ•˜์—ฌ ํฌ๋กฌ(ํฌ๋กœ๋ฏธ์›€ ๊ธฐ๋ฐ˜) ๋ธŒ๋ผ์šฐ์ €์ธ์ง€ ํŒ๋ณ„ + user_agent = request.headers.get("user-agent", "").lower() + is_chrome = "chrome" in user_agent if system == "Darwin": cameras = _avfoundation_cameras_in_cv2_order() @@ -1060,7 +1114,7 @@ def get_available_cameras(): if system == "Windows": return {"status": "success", "cameras": _windows_cameras()} if system == "Linux": - return {"status": "success", "cameras": _linux_cameras()} + return {"status": "success", "cameras": _linux_cameras(is_chrome=is_chrome)} import cv2 diff --git a/lelab/teleoperate.py b/lelab/teleoperate.py index 63416803..de09ded6 100644 --- a/lelab/teleoperate.py +++ b/lelab/teleoperate.py @@ -97,13 +97,13 @@ def get_joint_positions_from_robot(robot) -> dict[str, float]: raw_deg = observation[motor_key] angle_degrees = raw_deg - correction = _SO101_URDF_CORRECTIONS.get(motor_name) - if correction is not None and motor_name in calibration: - sign, urdf_zero_ticks = correction - cal = calibration[motor_name] - mid = (cal.range_min + cal.range_max) / 2 - motor_at_urdf_zero = (urdf_zero_ticks - mid) * 360 / _STS3215_MAX_RES - angle_degrees = sign * (raw_deg - motor_at_urdf_zero) + #correction = _SO101_URDF_CORRECTIONS.get(motor_name) + #if correction is not None and motor_name in calibration: + # sign, urdf_zero_ticks = correction + # cal = calibration[motor_name] + # mid = (cal.range_min + cal.range_max) / 2 + # motor_at_urdf_zero = (urdf_zero_ticks - mid) * 360 / _STS3215_MAX_RES + # angle_degrees = sign * (raw_deg - motor_at_urdf_zero) joint_positions[urdf_joint_name] = angle_degrees * math.pi / 180.0 debug_rows.append( diff --git a/lelab/utils/devices.py b/lelab/utils/devices.py index 3b949744..e6d3e092 100644 --- a/lelab/utils/devices.py +++ b/lelab/utils/devices.py @@ -49,3 +49,33 @@ def _force_close_device_resources(device: Any, logger: logging.Logger) -> None: cam.disconnect() except Exception as exc: logger.warning("Failed to disconnect camera after device cleanup failure: %s", exc) + + +def friendly_hint(error_text: str | None) -> str | None: + """A plain-language, actionable headline for the common SO-101 failures.""" + if not error_text: + return None + low = error_text.lower() + if "overload" in low or "torque_enable" in low: + return ( + "A motor overloaded โ€” usually the gripper holding an object too hard. Release the object / " + "open the gripper and power-cycle the arm before trying again." + ) + if "missing motor ids" in low or "motor check failed" in low: + return ( + "A follower motor isn't responding (often the gripper, id 6). If a skill was holding an object " + "it likely overloaded โ€” remove it, power-cycle the arm, then try teleoperation first." + ) + if "could not connect" in low or "failed to connect" in low or "not connected" in low: + return "Couldn't connect to the arm โ€” make sure it's plugged in, powered on, and on the right port." + if "frame is too old" in low or "no frame" in low or "frame timeout" in low: + return ( + "A camera can't keep up โ€” frames are arriving too slowly. Lower its resolution/FPS, " + "set FOURCC=MJPG, and close other heavy apps, then try again." + ) + if "failed to set capture_" in low or "actual_width" in low or "actual_height" in low: + return "A camera doesn't support the configured resolution โ€” open camera settings and click Auto." + if "permission" in low and ("port" in low or "com" in low): + return "Couldn't open the serial port โ€” close anything else using it, or run `lelab --stop`." + return None + diff --git a/study/assets/lelab_workflow.jpg b/study/assets/lelab_workflow.jpg new file mode 100644 index 00000000..472a09ce Binary files /dev/null and b/study/assets/lelab_workflow.jpg differ diff --git a/study/day0_overview.md b/study/day0_overview.md new file mode 100644 index 00000000..a295f2f0 --- /dev/null +++ b/study/day0_overview.md @@ -0,0 +1,87 @@ +# ๐Ÿฆพ LeLab: ๋ช…๋ น์ค„์€ ์žŠ์œผ์„ธ์š” +> **LeLab์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” โ€˜์ฝ”๋“œ ํ•œ ์ค„ ์—†๋Š”โ€™ ๋กœ๋ด‡ ํ•™์Šต ํ˜๋ช…** + +![LeLab: ์ฝ”๋“œ ์—†์ด ์‹œ์ž‘ํ•˜๋Š” AI ๋กœ๋ด‡ ์›Œํฌํ”Œ๋กœ์šฐ ๊ฐ€์ด๋“œ](assets/lelab_workflow.jpg) + +--- + +## 1. ์„œ๋ก : ๋กœ๋ด‡ ๊ณตํ•™์˜ ๋†’์€ ๋ฌธํ„ฑ์„ ํ—ˆ๋ฌผ๋‹ค + +๋กœ๋ด‡ ๊ณตํ•™์ด๋ผ๊ณ  ํ•˜๋ฉด ํ”ํžˆ ๊ฒ€์€ ํ™”๋ฉด์— ๊ฐ€๋“ํ•œ ๋ณต์žกํ•œ ์ฝ”๋“œ, ๋Š์ž„์—†์ด ํ„ฐ๋ฏธ๋„์— ์ž…๋ ฅํ•ด์•ผ ํ•˜๋Š” ๋ช…๋ น์–ด(CLI), ๊ทธ๋ฆฌ๊ณ  ์ง€๋‚œํ•œ ์บ˜๋ฆฌ๋ธŒ๋ ˆ์ด์…˜ ๊ณผ์ •์„ ๋– ์˜ฌ๋ฆฌ๊ณค ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ธฐ์ˆ ์  ์žฅ๋ฒฝ์€ ํ˜์‹ ์ ์ธ ์•„์ด๋””์–ด๋ฅผ ๊ฐ€์ง„ ์ด๋“ค์กฐ์ฐจ ๋กœ๋ด‡ ๊ณตํ•™์˜ ์ž…๊ตฌ์—์„œ ๋ฐœ๊ธธ์„ ๋Œ๋ฆฌ๊ฒŒ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. + +ํ•˜์ง€๋งŒ ์ด์ œ ์‹œ๋Œ€๊ฐ€ ๋ณ€ํ–ˆ์Šต๋‹ˆ๋‹ค. **LeLab**์€ *'๋ฐ•์Šค ๊ฐœ๋ด‰๋ถ€ํ„ฐ ํ•™์Šต๊นŒ์ง€ ๋‹จ ๋ช‡ ๋ถ„ ๋งŒ์—'*๋ผ๋Š” ๊ฐ€์น˜๋ฅผ ๋‚ด๊ฑธ๊ณ , ๋กœ๋ด‡ ํ•™์Šต์˜ ๋ชจ๋“  ๊ณผ์ •์„ ์ง๊ด€์ ์ธ ์›น ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์˜ฎ๊ฒจ์™”์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ LeLab์€ **SO-ARM101**(๋ฐ **SO-100** ์‹œ๋ฆฌ์ฆˆ)์— ์ตœ์ ํ™”๋˜์–ด, ํ•˜๋“œ์›จ์–ด ์„ค์ •๋ถ€ํ„ฐ AI ๋ชจ๋ธ ๋ฐฐํฌ๊นŒ์ง€์˜ ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ๋‹น์‹ ์˜ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋กœ๋ด‡์„ ๊ฐ€๋ฅด์น˜๋Š” ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. + +--- + +## 2. CLI ์—†์ด ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋๋‚ด๋Š” ๋กœ๋ด‡ ์›Œํฌํ”Œ๋กœ์šฐ + +LeLab์€ **LeRobot** ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ๋‹จ์ผ ์›น ์ธํ„ฐํŽ˜์ด์Šค๋กœ ํ†ตํ•ฉํ•œ ๊ณต์‹ GUI(๊ทธ๋ž˜ํ”ฝ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค)์ž…๋‹ˆ๋‹ค. ๊ณผ๊ฑฐ์—๋Š” ๊ฐ ๋‹จ๊ณ„๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ๋ณต์žกํ•œ ํ„ฐ๋ฏธ๋„ ๋ช…๋ น์–ด๋ฅผ ์ˆ™์ง€ํ•ด์•ผ ํ–ˆ์ง€๋งŒ, LeLab์€ ์ด๋ฅผ **"์›น ๋„ค์ดํ‹ฐ๋ธŒ(Web-native)"** ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค. ํŒŒ์ด์ฌ ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ ๋„๊ตฌ์ธ `uv`๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹จ ํ•œ ์ค„์˜ ๋ช…๋ น์–ด๋กœ ์„ค์น˜์™€ ์‹คํ–‰์„ ๋™์‹œ์— ๋๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +> [!NOTE] +> *"LeLab์€ LeRobot ์›Œํฌํ”Œ๋กœ์šฐ ์ „์ฒด(๊ต์ •, ํ…”๋ ˆ์˜คํผ๋ ˆ์ด์…˜, ๋…นํ™”, ํ•™์Šต, ์žฌ์ƒ)๋ฅผ ๋‹จ์ผ ๋ธŒ๋ผ์šฐ์ € UI์— ๋‹ด์€ ์›น ์•ฑ์ž…๋‹ˆ๋‹ค. ํŒ”์„ ์—ฐ๊ฒฐํ•˜๊ณ  ์•ฑ์„ ์—ด๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. CLI๋ฅผ ํ†ตํ•œ ๋ณต์žกํ•œ ์กฐ์ž‘(CLI gymnastics)๋„, ํ‚ค๋ณด๋“œ ํ”„๋กฌํ”„ํŠธ๋„ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค."* + +ํŠนํžˆ ์ž…๋ฌธ์ž๋“ค์ด ๊ฐ€์žฅ ์–ด๋ ค์›Œํ•˜๋Š” **'ํฌํŠธ ์ฐพ๊ธฐ'**์™€ **'๊ต์ •(Calibration)'** ๊ณผ์ •์„ ํš๊ธฐ์ ์œผ๋กœ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค. +* **Unplug-to-detect ๊ธฐ๋Šฅ**: USB ์ผ€์ด๋ธ”์„ ๋ฝ‘์•˜๋‹ค๊ฐ€ ๋‹ค์‹œ ๊ฝ‚๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ํฌํŠธ๋ฅผ ์ž๋™ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค. +* **์‹œ๊ฐ์  ๊ต์ • ํ”„๋กœ์„ธ์Šค**: ๋ชจ๋“  ๊ด€์ ˆ์„ ์ค‘๊ฐ„ ์œ„์น˜(Middle position)์— ์ •๋ ฌํ•˜๊ณ  ๊ฐ€์ด๋“œ์— ๋”ฐ๋ผ ์›€์ง์ด๊ธฐ๋งŒ ํ•˜๋ฉด ๋๋‚ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ํ˜„์žฅ์—์„œ์˜ ๋งˆ์ฐฐ๋ ฅ์„ "์ œ๋กœ"์— ๊ฐ€๊น๊ฒŒ ์ค„์—ฌ์ค๋‹ˆ๋‹ค. + +--- + +## 3. ํ…”๋ ˆ์˜คํผ๋ ˆ์ด์…˜: 3D ์‹œ๊ฐํ™”๊ฐ€ ๋งŒ๋“œ๋Š” ๋ฐ์ดํ„ฐ์˜ ์ •๋ฐ€๋„ + +๋กœ๋ด‡์„ ํ•™์Šต์‹œํ‚ค๊ธฐ ์ „, ์ธ๊ฐ„์˜ ๋™์ž‘์„ ๋กœ๋ด‡์—๊ฒŒ ์ „๋‹ฌํ•˜๋Š” **'ํ…”๋ ˆ์˜คํผ๋ ˆ์ด์…˜(Teleoperation)'**์€ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. LeLab์€ **๋ฆฌ๋”-ํŒ”๋กœ์›Œ(Leader-Follower)** ๊ตฌ์กฐ๋ฅผ ์ง€์›ํ•˜์—ฌ, ์‚ฌ์šฉ์ž๊ฐ€ ๋ฆฌ๋” ๋กœ๋ด‡์„ ์›€์ง์ด๋ฉด ํŒ”๋กœ์›Œ ๋กœ๋ด‡์ด ์ด๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณต์ œํ•ฉ๋‹ˆ๋‹ค. + +์—ฌ๊ธฐ์„œ LeLab์˜ ์ง„๊ฐ€๋Š” **์‹ค์‹œ๊ฐ„ 3D ์‹œ๊ฐํ™”**์—์„œ ๋“œ๋Ÿฌ๋‚ฉ๋‹ˆ๋‹ค. ๋‹จ์ˆœํžˆ ํ™”๋ฉด์„ ๋ณด๋Š” ๊ฒƒ์„ ๋„˜์–ด, 3D ๋ชจ๋ธ์„ ํ†ตํ•ด ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ ์‹œ์•ผ๊ฐ€ ๊ฐ€๋ ค์ง€๋Š” ์‚ฌ๊ฐ์ง€๋Œ€(Occlusion)๋‚˜ ๋กœ๋ด‡ ๊ด€์ ˆ์˜ ํ•œ๊ณ„ ๊ฐ๋„๋ฅผ ์ง๊ด€์ ์œผ๋กœ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +> [!TIP] +> ์ด๋Š” ๋‹จ์ˆœํ•œ ์‹œ๊ฐ์  ํšจ๊ณผ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์—…์„ **'์™„๋ฒฝํ•˜๊ณ  ๋ถ€๋“œ๋Ÿฝ๊ฒŒ(flawlessly and smoothly)'** ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋•๋Š” ์—ฐ์Šต ๋„๊ตฌ์ด๋ฉฐ, ๊ฒฐ๊ตญ AI ๋ชจ๋ธ์˜ ์„ฑ๋Šฅ์„ ๊ฒฐ์ •์ง“๋Š” ๊ณ ํ’ˆ์งˆ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋ฐ˜์ด ๋ฉ๋‹ˆ๋‹ค. + +--- + +## 4. ๊ณ ์„ฑ๋Šฅ ์ •์ฑ…์˜ ๋น„๋ฐ€: '50๋ฒˆ์˜ ์ผ๊ด€๋œ ๋ฐ˜๋ณต'๊ณผ ํ’ˆ์งˆ ๊ด€๋ฆฌ + +์„ฑ๊ณต์ ์ธ AI ๋ชจ๋ธ(Policy)์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ฐ์ดํ„ฐ์˜ '์–‘'๋ณด๋‹ค **'์งˆ'**๊ณผ **'์ผ๊ด€์„ฑ'**์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. LeLab์€ ์ด ๊ณผ์ •์„ ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ตœ์ ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค. + +* **์„ฑ๊ณต์ ์ธ ํ•™์Šต์„ ์œ„ํ•œ ๊ณจ๋“  ๋„˜๋ฒ„**: LeLab์€ ํ•˜๋‚˜์˜ ์ž‘์—…์„ ์œ„ํ•ด ์ตœ์†Œ 30ํšŒ, ์•ˆ์ •์ ์ธ ์„ฑ๋Šฅ์„ ์œ„ํ•ด **50ํšŒ์˜ ์—ํ”ผ์†Œ๋“œ ๋…นํ™”**๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋ฐ์ดํ„ฐ์…‹์˜ ๋…ธ์ด์ฆˆ๋ฅผ ์ค„์ด๊ณ  ๋ชจ๋ธ์ด ๋™์ž‘์˜ ํŒจํ„ด์„ ํ™•๋ฆฝํ•˜๊ธฐ ์œ„ํ•œ ์ตœ์ ์˜ ์ˆ˜์น˜์ž…๋‹ˆ๋‹ค. +* **์‹ค์‹œ๊ฐ„ ํ’ˆ์งˆ ๊ด€๋ฆฌ**: ๋…นํ™” ์ค‘ ์‹ค์ˆ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค๋ฉด ์ „์ฒด๋ฅผ ์ƒˆ๋กœ ์‹œ์ž‘ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ธํ„ฐํŽ˜์ด์Šค์˜ ๋ฉ”๋‰ด๋‚˜ ๋‹จ์ถ•ํ‚ค๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ์—ํ”ผ์†Œ๋“œ๋งŒ ์ฆ‰์‹œ **์žฌ๋…นํ™”(Re-record)**ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +* **์›Œํฌํ”Œ๋กœ์šฐ ์ตœ์ ํ™”**: ์ŠคํŽ˜์ด์Šค๋ฐ” ํ•˜๋‚˜๋กœ ์—ํ”ผ์†Œ๋“œ ์ „ํ™˜๊ณผ ํ™˜๊ฒฝ ๋ฆฌ์…‹ ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด, ํŽœ์„ ์˜ฎ๊ธฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ์ž‘์—…์„ 50๋ฒˆ ๋ฐ˜๋ณตํ•˜๋Š” ๋ฐ ๋‹จ **15๋ถ„**์ด๋ฉด ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 5. ๋กœ์ปฌ์„ ๋„˜์–ด ํด๋ผ์šฐ๋“œ๋กœ: HF Jobs์™€์˜ ๋งค๋„๋Ÿฌ์šด ํ†ตํ•ฉ + +๋กœ๋ด‡ ํ•™์Šต์—๋Š” ๊ฐ•๋ ฅํ•œ GPU ์ž์›์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. LeLab์€ ๊ฐœ์ธ PC ์‚ฌ์–‘์˜ ํ•œ๊ณ„๋ฅผ **Hugging Face Jobs (HF Jobs)**์™€์˜ ํ†ตํ•ฉ์œผ๋กœ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค. ํ„ฐ๋ฏธ๋„์—์„œ `hf auth login`์„ ํ†ตํ•ด ํ•œ ๋ฒˆ๋งŒ ์ธ์ฆํ•˜๋ฉด, ํด๋ฆญ ๋ช‡ ๋ฒˆ์œผ๋กœ Hugging Face์˜ ํด๋ผ์šฐ๋“œ GPU์—์„œ ํ•™์Šต์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +์ด ๋ฐฉ์‹์˜ ๊ฐ•์ ์€ **'ํ™•์žฅ์„ฑ'**์ž…๋‹ˆ๋‹ค. ๋กœ์ปฌ ํ•˜๋“œ์›จ์–ด๋ฅผ ์ ์œ ํ•˜์ง€ ์•Š๊ณ ๋„ ์—ฌ๋Ÿฌ ํ•™์Šต ์„ธ์…˜์„ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ•™์Šต ๋กœ๊ทธ์™€ ์†์‹ค ์ˆ˜์น˜(Loss)๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณ ๊ฐ€์˜ ์›Œํฌ์Šคํ…Œ์ด์…˜ ์—†์ด๋„ ๋ˆ„๊ตฌ๋‚˜ ์ตœ์ฒจ๋‹จ ๋กœ๋ด‡ AI๋ฅผ ํ•™์Šต์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ์†Œํ”„ํŠธ์›จ์–ด ์ค‘์‹ฌ์˜ ๋ฏผ์ฃผํ™”๊ฐ€ ์ด๋ฃจ์–ด์ง„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. + +--- + +## 6. ํ•™์Šต ์ค‘์— ๋ฐ”๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ์‹ค์‹œ๊ฐ„ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„ + +LeLab์˜ ๊ฐ€์žฅ ํ˜์‹ ์ ์ธ ๊ธฐ๋Šฅ์€ ํ•™์Šต์ด ์™„์ „ํžˆ ์ข…๋ฃŒ๋˜๊ธฐ ์ „์—๋„ **์ฒดํฌํฌ์ธํŠธ๋ฅผ ํ™œ์šฉํ•ด ์ฆ‰์‹œ ์ถ”๋ก (Inference)์„ ์‹คํ–‰**ํ•ด ๋ณผ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๋ฉฐ์น ์”ฉ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ๋Š” ํ•™์Šต ๊ณผ์ •์—์„œ ์ž˜๋ชป๋œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ธํ•œ ์‹œ๊ฐ„ ๋‚ญ๋น„๋ฅผ ๋ฐฉ์ง€ํ•ด ์ค๋‹ˆ๋‹ค. + +์‹ค์ œ๋กœ **ACT Policy (Action Chunking with Transformers)**๋ฅผ ์‚ฌ์šฉํ•ด ํ•™์Šตํ•  ๋•Œ: +* **3,000 ์Šคํ… ์‹œ์ **: ๋กœ๋ด‡์ด ํŽœ์„ ์ œ๋Œ€๋กœ ์žก์ง€ ๋ชปํ•˜๊ณ  ๋ฐฉํ™ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +* **30,000 ์Šคํ… ์‹œ์ **: ํ•™์Šต์ด ์ง„ํ–‰๋จ์— ๋”ฐ๋ผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜๋Š” ์ฒดํฌํฌ์ธํŠธ๋ฅผ ์ ์šฉํ•ด ๋ณด๋ฉด, ๋กœ๋ด‡์ด ํ™˜๊ฒฝ์˜ ๋ฏธ์„ธํ•œ ๋ณ€ํ™”์— ๋Œ€์‘ํ•˜๋ฉฐ ํŽœ์„ ์ •ํ™•ํžˆ ์ง‘์–ด ๋“œ๋Š” '์ง„๋ณด์˜ ์ˆœ๊ฐ„'์„ ๋ชฉ๊ฒฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. + +์ด๋Ÿฌํ•œ ์‹ค์‹œ๊ฐ„ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„๋Š” ๋กœ๋ด‡ ํ•™์Šต์„ ์ง€๋ฃจํ•œ ๊ธฐ๋‹ค๋ฆผ์ด ์•„๋‹Œ ์—ญ๋™์ ์ธ ์‹คํ—˜์˜ ๊ณผ์ •์œผ๋กœ ๋ฐ”๊ฟ” ๋†“์Šต๋‹ˆ๋‹ค. + +--- + +## 7. ๊ฒฐ๋ก : ๋‹น์‹ ์˜ ์ฑ…์ƒ ์œ„์—์„œ ์‹œ์ž‘๋  ๋กœ๋ด‡ ์‹œ๋Œ€ + +LeLab์€ ์˜คํ”ˆ ์†Œ์Šค ์ปค๋ฎค๋‹ˆํ‹ฐ์˜ ์—ด์ •๊ณผ Hugging Face์˜ ๊ธฐ์ˆ ๋ ฅ์ด ๊ฒฐํ•ฉํ•˜์—ฌ ํƒ„์ƒํ•œ ๊ฒฐ๊ณผ๋ฌผ์ž…๋‹ˆ๋‹ค. ๋ณต์žกํ•œ ๋ช…๋ น์ค„ ํ™˜๊ฒฝ๊ณผ ์ฝ”๋“œ์˜ ์žฅ๋ฒฝ์„ ํ—ˆ๋ฌผ๊ณ  ์ง๊ด€์ ์ธ ์›น UI๋ฅผ ์ œ๊ณตํ•จ์œผ๋กœ์จ, ์ด์ œ ๋ˆ„๊ตฌ๋‚˜ ์ž์‹ ์˜ ์ฑ…์ƒ ์œ„์—์„œ ๋กœ๋ด‡ AI๋ฅผ ์—ฐ๊ตฌํ•˜๊ณ  ์‹คํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์‹œ๋Œ€๊ฐ€ ์—ด๋ ธ์Šต๋‹ˆ๋‹ค. + +์„ค์น˜๋ถ€ํ„ฐ ์ฒซ ๋ชจ๋ธ์˜ ์„ฑ๊ณต์ ์ธ ์ถ”๋ก ๊นŒ์ง€, LeLab์€ ๋‹น์‹ ์˜ ๊ฐ€์žฅ ์นœ์ ˆํ•œ ๊ฐ€์ด๋“œ๊ฐ€ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. + +--- + +## 8. ์ฐธ๊ณ  ์ž๋ฃŒ ๋ฐ ๊ด€๋ จ ๋งํฌ + +* **[๐Ÿ“บ LeLab ์†Œ๊ฐœ ๋ฐ ์‹ค์‚ฌ์šฉ ํŠœํ† ๋ฆฌ์–ผ ์˜์ƒ (YouTube)](https://youtu.be/VqyKUuW9V1g?si=VR9VXvA3JwxR_E0-)** +* **[GitHub - LeRobot](https://github.com/huggingface/lerobot)**: LeLab์˜ ๊ธฐ๋ฐ˜์ด ๋˜๋Š” ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ +* **[Hugging Face Space - LeLab](https://huggingface.co/spaces/lerobot/LeLab)**: ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง์ ‘ UI ์ฒดํ—˜ํ•˜๊ธฐ +* **[LeRobot Discord Community](https://discord.gg/q8Dzzpym3f)**: ์ „ ์„ธ๊ณ„ ๊ฐœ๋ฐœ์ž ๋ฐ ๋กœ๋ด‡ ๊ณตํ•™ ์ปค๋ฎค๋‹ˆํ‹ฐ + +--- + +> **"์ฝ”๋“œ ํ•œ ์ค„ ์—†์ด ๋‹น์‹ ์˜ ๋กœ๋ด‡์—๊ฒŒ ๊ฐ€๋ฅด์น˜๊ณ  ์‹ถ์€ ์ฒซ ๋ฒˆ์งธ ์ž‘์—…์€ ๋ฌด์—‡์ธ๊ฐ€์š”? ์ง€๊ธˆ ๋ฐ”๋กœ LeLab๊ณผ ํ•จ๊ป˜ ๋กœ๋ด‡ ๊ณตํ•™์˜ ๋ฏธ๋ž˜์— ์ ‘์†ํ•ด ๋ณด์‹ญ์‹œ์˜ค."** diff --git a/study/install.md b/study/install.md new file mode 100644 index 00000000..4034c759 --- /dev/null +++ b/study/install.md @@ -0,0 +1,117 @@ +# ๐Ÿฆพ LeLab Windows ์„ค์น˜ ๋ฐ ์‹คํ–‰ ๊ฐ€์ด๋“œ (Windows Installation Guide) + +์ด ๋ฌธ์„œ๋Š” **Windows ํ™˜๊ฒฝ**์—์„œ **LeLab**(LeRobot์˜ ๊ณต์‹ ์›น ๊ทธ๋ž˜ํ”ฝ ์ธํ„ฐํŽ˜์ด์Šค)์„ ์„ค์น˜ํ•˜๊ณ  ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. ๊ฐ•์˜ ๋‚ด์šฉ ๋ฐ ๊ฐœ๋ฐœ ํ‘œ์ค€(CLAUDE.md)์„ ๋ฐ”ํƒ•์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +--- + +## ๐Ÿ“‹ ์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ (Prerequisites) + +- **OS**: Windows 10 ์ด์ƒ +- **Python**: 3.12 ์ด์ƒ (๊ถŒ์žฅ) +- **Git**: ์†Œ์Šค ์ฝ”๋“œ ๋ณต์ œ์šฉ +- **uv**: ๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์ธ ํŒจํ‚ค์ง€ ๋ฐ ๊ฐ€์ƒํ™˜๊ฒฝ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ๊ฐ•๋ ฅํžˆ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. + +--- + +## โš™๏ธ 1๋‹จ๊ณ„: ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ์„ฑ + +### 1. `uv` ์„ค์น˜ (๋ฏธ์„ค์น˜๋œ ๊ฒฝ์šฐ) +PowerShell์„ ์—ด๊ณ  ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜์—ฌ `uv`๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค: +```powershell +powershell -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +### 2. ๊ฐ€์ƒํ™˜๊ฒฝ(Virtual Environment) ์ƒ์„ฑ +ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ํด๋”(`leLab/`)๋กœ ์ด๋™ํ•˜์—ฌ Python 3.12 ๊ธฐ๋ฐ˜์˜ ๊ฐ€์ƒํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. (`uv`๋Š” ํ•„์š”ํ•œ Python ๋ฒ„์ „์„ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜์—ฌ ๋‹ค์šด๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.) +```powershell +uv venv --python 3.12 +``` + +### 3. ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” (Activate) +- **PowerShell**: + ```powershell + .venv\Scripts\Activate.ps1 + ``` +- **๊ธฐ๋ณธ CMD**: + ```cmd + .venv\Scripts\activate.bat + ``` + +--- + +## ๐Ÿ“ฅ 2๋‹จ๊ณ„: ํŒจํ‚ค์ง€ ๋ฐ ์ข…์†์„ฑ ์„ค์น˜ + +Windows ํ™˜๊ฒฝ ์ „์šฉ ์นด๋ฉ”๋ผ ์ง€์› ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(`pygrabber`)์™€ ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ ๋„๊ตฌ๋ฅผ ํฌํ•จํ•˜์—ฌ **Editable ๋ชจ๋“œ(-e)**๋กœ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค: + +```powershell +# ๊ฐœ๋ฐœ ๋ฐ ํ…Œ์ŠคํŠธ ์ข…์†์„ฑ์„ ํ•จ๊ป˜ ์„ค์น˜ +uv pip install -e ".[dev,test]" +``` + +> [!NOTE] +> - Windows ํ™˜๊ฒฝ์—์„œ๋Š” ์นด๋ฉ”๋ผ์˜ ๋ธŒ๋ผ์šฐ์ € `deviceId`์™€ ์‹ค์ œ DirectShow ์นด๋ฉ”๋ผ ์ด๋ฆ„์„ ๋งคํ•‘ํ•˜๊ธฐ ์œ„ํ•ด `pygrabber` ํŒจํ‚ค์ง€๊ฐ€ ์ž๋™์œผ๋กœ ํ•จ๊ป˜ ์„ค์น˜๋ฉ๋‹ˆ๋‹ค. +> - `lerobot` ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” `pyproject.toml`์— ๋ช…์‹œ๋œ Hugging Face ๊ณต์‹ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์˜ ํŠน์ • ์ปค๋ฐ‹ ๋ฒ„์ „์„ ์ฐธ์กฐํ•˜์—ฌ ์ž๋™์œผ๋กœ ๋นŒ๋“œ ๋ฐ ์„ค์น˜๋ฉ๋‹ˆ๋‹ค. + +--- + +## ๐Ÿš€ 3๋‹จ๊ณ„: LeLab ์‹คํ–‰ํ•˜๊ธฐ + +๊ฐ€์ƒํ™˜๊ฒฝ์ด ํ™œ์„ฑํ™”๋œ ์ƒํƒœ์—์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•ฑ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +### A. ์ผ๋ฐ˜ ๋ชจ๋“œ (์„œ๋ฒ„ & ๋นŒ๋“œ๋œ ํ”„๋ก ํŠธ์—”๋“œ ํ†ตํ•ฉ ์‹คํ–‰) +Vite๋กœ ๋นŒ๋“œ๋œ ์ •์  ํ”„๋ก ํŠธ์—”๋“œ ํŒŒ์ผ๊ณผ FastAPI ๋ฐฑ์—”๋“œ๊ฐ€ ํฌํŠธ `8000`์—์„œ ๋™์‹œ์— ์‹คํ–‰๋˜๋ฉฐ, ๋ธŒ๋ผ์šฐ์ € ์ฐฝ์ด ์ž๋™์œผ๋กœ ์—ด๋ฆฝ๋‹ˆ๋‹ค. +```powershell +lelab +``` + +### B. ๊ฐœ๋ฐœ์ž ๋ชจ๋“œ (ํ•ซ ๋ฆฌ๋กœ๋“œ ๋ฐ ์‹ค์‹œ๊ฐ„ ์ฝ”๋“œ ์ˆ˜์ •) +๋ฐฑ์—”๋“œ FastAPI ์„œ๋ฒ„(`:8000`, ์ž๋™ ์žฌ์‹คํ–‰ ์ ์šฉ)์™€ ํ”„๋ก ํŠธ์—”๋“œ Vite ๊ฐœ๋ฐœ ์„œ๋ฒ„(`:8080`)๋ฅผ ๋™์‹œ์— ๋„์šฐ๋ฉฐ, ์ฝ”๋“œ ์ˆ˜์ • ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™”๋ฉด์— ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. +```powershell +lelab --dev +``` + +--- + +## ๐Ÿ’ก Windows ํ™˜๊ฒฝ ํ•ต์‹ฌ ํ•ต์‹ฌ ํฌ์ธํŠธ ๋ฐ ๋ฌธ์ œ ํ•ด๊ฒฐ + +### 1. ํฌํŠธ ์ž๋™ ์ธ์‹ (Unplug-to-detect) +๋กœ๋ด‡ ํŒ”(SO-101 ๋ฆฌ๋”/ํŒ”๋กœ์›Œ) ์—ฐ๊ฒฐ ์‹œ ์–ด๋–ค ์‹œ๋ฆฌ์–ผ ํฌํŠธ(COM Port)์— ์—ฐ๊ฒฐ๋˜์—ˆ๋Š”์ง€ ์ผ์ผ์ด ์ฐพ์„ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. USB ์ผ€์ด๋ธ”์„ ๋ฝ‘์•˜๋‹ค๊ฐ€ ๋‹ค์‹œ ๊ฝ‚์œผ๋ฉด ํ™”๋ฉด์—์„œ ์ž๋™์œผ๋กœ ํฌํŠธ๋ฅผ ์ธ์‹ํ•˜์—ฌ ๊ต์ •(Calibration)์„ ๋ฐ”๋กœ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +### 2. ์„ค์ • ๋ฐ ๊ต์ • ํŒŒ์ผ ์ €์žฅ ๊ฒฝ๋กœ +Windows์—์„œ ๊ต์ • ๋ฐ์ดํ„ฐ ๋ฐ ์„ค์ • ํŒŒ์ผ๋“ค์€ ๋‹ค์Œ ๊ฒฝ๋กœ์— ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ ๋ฐ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค: +- `C:\Users\<์‚ฌ์šฉ์ž์ด๋ฆ„>\.cache\huggingface\lerobot\calibration\` (๋ฆฌ๋”/ํŒ”๋กœ์›Œ ์กฐ์ธํŠธ ์„ค์ • ๋“ฑ) +- `C:\Users\<์‚ฌ์šฉ์ž์ด๋ฆ„>\.cache\huggingface\lerobot\ports\` (์ตœ๊ทผ ์‚ฌ์šฉ ํฌํŠธ ๊ธฐ๋ก) + +### 3. ๋ชจ๋ฐ”์ผ ์นด๋ฉ”๋ผ ์ŠคํŠธ๋ฆฌ๋ฐ (HTTPS ๊ตฌ์„ฑ) +์Šค๋งˆํŠธํฐ ์นด๋ฉ”๋ผ๋ฅผ ๋กœ๋ด‡ ํ•™์Šต์šฉ ์™ธ๋ถ€ ์นด๋ฉ”๋ผ๋กœ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋ธŒ๋ผ์šฐ์ € ๋ณด์•ˆ ์ •์ฑ…์ƒ **HTTPS** ํ™˜๊ฒฝ์ด ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. +- self-signed ์ธ์ฆ์„œ ํŒŒ์ผ์„ `certs/` ๋””๋ ‰ํ† ๋ฆฌ์— ๋ฐฐ์น˜ํ•œ ํ›„ uvicorn์„ ์ˆ˜๋™์œผ๋กœ ๊ตฌ๋™ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: + ```powershell + uvicorn lelab.server:app --ssl-keyfile certs/key.pem --ssl-certfile certs/cert.pem --host 0.0.0.0 --port 8000 + ``` + +### 4. ํ—ˆ๊น…ํŽ˜์ด์Šค CLI ๋กœ๊ทธ์ธ (Hugging Face CLI Authentication) +LeLab์˜ ํด๋ผ์šฐ๋“œ GPU ํ•™์Šต ๊ธฐ๋Šฅ(HF Jobs)์ด๋‚˜ ๋ฐ์ดํ„ฐ์…‹ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด Hugging Face ๊ณ„์ • ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. + +Hugging Face CLI์ธ `hf` ๋„๊ตฌ๋Š” **์ด๋ฏธ ๊ฐ€์ƒํ™˜๊ฒฝ ๋‚ด์— ์„ค์น˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค**. ๋”ฐ๋ผ์„œ ๋ณ„๋„์˜ ์ถ”๊ฐ€ ์„ค์น˜ ์—†์ด ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” ์ƒํƒœ์—์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋กœ ๋กœ๊ทธ์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: + +1. **๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” ์ƒํƒœ**์˜ ํ„ฐ๋ฏธ๋„์—์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค: + ```powershell + hf auth login + ``` +2. ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋‚˜ํƒ€๋‚˜๋ฉด [Hugging Face Settings -> Access Tokens](https://huggingface.co/settings/tokens)์—์„œ ๋ฐœ๊ธ‰๋ฐ›์€ **Access Token (Write ๊ถŒํ•œ ๊ถŒ์žฅ)**์„ ๋ณต์‚ฌํ•˜์—ฌ ๋ถ™์—ฌ๋„ฃ์Šต๋‹ˆ๋‹ค. (Windows PowerShell์—์„œ๋Š” ๋งˆ์šฐ์Šค ์šฐํด๋ฆญ์œผ๋กœ ๋ถ™์—ฌ๋„ฃ๊ธฐ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ๋ณด์•ˆ์ƒ ์ž…๋ ฅ ์ค‘์ธ ๋ฌธ์ž๋‚˜ ๋ณ„ํ‘œ๋Š” ํ™”๋ฉด์— ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.) +3. **๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ**: ์ œ๋Œ€๋กœ ์—ฐ๋™์ด ๋˜์—ˆ๋Š”์ง€ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค: + ```powershell + hf auth whoami + ``` + +๋งŒ์•ฝ ๊ฐ€์ƒํ™˜๊ฒฝ ์™ธ๋ถ€(๊ธ€๋กœ๋ฒŒ ํ™˜๊ฒฝ)์—์„œ ๊ฐœ๋ณ„์ ์œผ๋กœ Hugging Face CLI๋ฅผ ๊ด€๋ฆฌ/์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด `uv`๋ฅผ ์‚ฌ์šฉํ•ด ๊ธ€๋กœ๋ฒŒ ๋„๊ตฌ๋กœ ์„ค์น˜ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค: +```powershell +# ๊ธ€๋กœ๋ฒŒ ํ™˜๊ฒฝ์— hf CLI ์„ค์น˜ +uv tool install huggingface_hub + +# ๋กœ๊ทธ์ธ ์‹คํ–‰ +hf auth login + +# ๋กœ๊ทธ์ธ ํ™•์ธ +hf auth whoami +``` diff --git a/tests/test_record.py b/tests/test_record.py index 486eb959..62e83953 100644 --- a/tests/test_record.py +++ b/tests/test_record.py @@ -119,3 +119,28 @@ def test_build_camera_configs_skips_non_opencv_type() -> None: configs = _build_camera_configs(cameras, Cv2Backends.ANY) assert configs == {} + + +def test_recording_status_surfaces_error_and_hint(monkeypatch: pytest.MonkeyPatch) -> None: + """When the recording session ends with an error, handle_recording_status must + correctly return the error traceback/message and a friendly troubleshooting hint. + """ + import lelab.record as record + + # Force error state + monkeypatch.setattr(record, "current_phase", "error") + monkeypatch.setattr(record, "recording_active", False) + monkeypatch.setattr( + record, + "last_recording_info", + {"success": False, "error": "RuntimeError: Motor 6 overload, torque_enable failed"}, + ) + + result = record.handle_recording_status() + assert result["recording_active"] is False + assert result["current_phase"] == "error" + assert result["session_ended"] is True + assert "Motor 6 overload" in result["error"] + assert result["hint"] is not None + assert "overloaded" in result["hint"].lower() + diff --git a/tests/test_rollout.py b/tests/test_rollout.py index c0bb1f3d..063cb6ab 100644 --- a/tests/test_rollout.py +++ b/tests/test_rollout.py @@ -281,12 +281,12 @@ def test_classify_outcome_ok_warns_and_fails() -> None: def test_friendly_hint_maps_common_failures() -> None: - from lelab.rollout import _friendly_hint + from lelab.utils.devices import friendly_hint - assert "gripper" in (_friendly_hint("Motor overload detected") or "").lower() - assert "connect" in (_friendly_hint("Failed to connect to the follower") or "").lower() - assert _friendly_hint("some unrecognised traceback") is None - assert _friendly_hint(None) is None + assert "gripper" in (friendly_hint("Motor overload detected") or "").lower() + assert "connect" in (friendly_hint("Failed to connect to the follower") or "").lower() + assert friendly_hint("some unrecognised traceback") is None + assert friendly_hint(None) is None def test_extract_error_from_log_pulls_exception_tail(tmp_path) -> None: