openral-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openral_cli/__init__.py +6 -0
- openral_cli/_hf_publish.py +125 -0
- openral_cli/_rskill_doc_validator.py +562 -0
- openral_cli/_rskill_intel.py +334 -0
- openral_cli/_rskill_readme.py +217 -0
- openral_cli/_rskill_scaffolder.py +267 -0
- openral_cli/autodetect.py +464 -0
- openral_cli/check.py +397 -0
- openral_cli/collision.py +369 -0
- openral_cli/dataset.py +438 -0
- openral_cli/deploy_sim.py +2664 -0
- openral_cli/install.py +422 -0
- openral_cli/main.py +4108 -0
- openral_cli/prompt.py +128 -0
- openral_cli/py.typed +0 -0
- openral_cli/robot.py +301 -0
- openral_cli-0.1.0.dist-info/METADATA +33 -0
- openral_cli-0.1.0.dist-info/RECORD +20 -0
- openral_cli-0.1.0.dist-info/WHEEL +4 -0
- openral_cli-0.1.0.dist-info/entry_points.txt +2 -0
openral_cli/dataset.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""``openral dataset`` Typer app.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
|
|
5
|
+
* ``push <root>`` — upload a local LeRobotDataset v3 to the HF Hub.
|
|
6
|
+
Always ``private=True``; a typed consent prompt discloses the PII risks
|
|
7
|
+
(faces / room layouts / biometric joint trajectories) before any
|
|
8
|
+
network call. ``--yes`` or ``OPENRAL_DATASET_CONSENT=1`` skips the
|
|
9
|
+
prompt for CI / scripted invocations; ``--dry-run`` short-circuits
|
|
10
|
+
the consent gate entirely and just validates the dataset.
|
|
11
|
+
|
|
12
|
+
The PR4 ``from-bag`` subcommand will be added in the rosbag2 converter
|
|
13
|
+
PR. ``push`` ships first because it has no ROS dependency surface and
|
|
14
|
+
unblocks the dataset-flywheel demo regardless of rosbag2 readiness.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Final
|
|
24
|
+
|
|
25
|
+
import structlog
|
|
26
|
+
import typer
|
|
27
|
+
from openral_core.exceptions import ROSConfigError, ROSError
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
from rich.panel import Panel
|
|
30
|
+
|
|
31
|
+
from openral_cli._hf_publish import IGNORE_PATTERNS, ensure_private, resolve_token
|
|
32
|
+
|
|
33
|
+
__all__ = ["dataset_app"]
|
|
34
|
+
|
|
35
|
+
_log = structlog.get_logger(__name__)
|
|
36
|
+
_console = Console()
|
|
37
|
+
|
|
38
|
+
# Env-var override for the consent prompt. Lets CI runs / scripted
|
|
39
|
+
# uploads avoid the typer.confirm() interactive prompt.
|
|
40
|
+
_CONSENT_ENV_VAR: Final[str] = "OPENRAL_DATASET_CONSENT"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
dataset_app = typer.Typer(
|
|
44
|
+
name="dataset",
|
|
45
|
+
help=(
|
|
46
|
+
"Publish or convert OpenRAL datasets.\n"
|
|
47
|
+
"\n"
|
|
48
|
+
"Commands:\n"
|
|
49
|
+
" push — upload a local LeRobotDataset v3 to the HF Hub (private; consent-gated).\n"
|
|
50
|
+
" from-bag — convert an mcap rosbag2 written by Rosbag2Sink into a LeRobotDataset v3."
|
|
51
|
+
),
|
|
52
|
+
no_args_is_help=True,
|
|
53
|
+
add_completion=False,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── from-bag subcommand (PR4) ────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataset_app.command("from-bag")
|
|
61
|
+
def from_bag_command(
|
|
62
|
+
bag_path: Path = typer.Argument(
|
|
63
|
+
...,
|
|
64
|
+
help="Input .mcap bag written by openral_dataset.Rosbag2Sink.",
|
|
65
|
+
exists=True,
|
|
66
|
+
file_okay=True,
|
|
67
|
+
dir_okay=False,
|
|
68
|
+
resolve_path=True,
|
|
69
|
+
),
|
|
70
|
+
robot_yaml: Path = typer.Option(
|
|
71
|
+
...,
|
|
72
|
+
"--robot",
|
|
73
|
+
help=(
|
|
74
|
+
"Path to a RobotDescription robot.yaml — used to bind feature "
|
|
75
|
+
"shapes (state_shape from ObservationSpec, dim from ActionSpec, "
|
|
76
|
+
"camera keys from SensorSpec.vla_feature_key). Must match the "
|
|
77
|
+
"robot used at record time."
|
|
78
|
+
),
|
|
79
|
+
exists=True,
|
|
80
|
+
file_okay=True,
|
|
81
|
+
dir_okay=False,
|
|
82
|
+
resolve_path=True,
|
|
83
|
+
),
|
|
84
|
+
output_root: Path = typer.Option(
|
|
85
|
+
...,
|
|
86
|
+
"--output",
|
|
87
|
+
"-o",
|
|
88
|
+
help=(
|
|
89
|
+
"Destination LeRobotDataset v3 root directory. Must NOT pre-exist "
|
|
90
|
+
"(lerobot v3 refuses to write into a populated root)."
|
|
91
|
+
),
|
|
92
|
+
resolve_path=True,
|
|
93
|
+
),
|
|
94
|
+
repo_id: str | None = typer.Option(
|
|
95
|
+
None,
|
|
96
|
+
"--repo-id",
|
|
97
|
+
help=(
|
|
98
|
+
"Override the resulting dataset's repo_id. Defaults to openral/dataset-<robot_name>."
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
license_str: str = typer.Option(
|
|
102
|
+
"CC-BY-4.0",
|
|
103
|
+
"--license",
|
|
104
|
+
help="SPDX license string for the produced dataset.",
|
|
105
|
+
),
|
|
106
|
+
fps: float | None = typer.Option(
|
|
107
|
+
None,
|
|
108
|
+
"--fps",
|
|
109
|
+
help=(
|
|
110
|
+
"Frames-per-second for the produced dataset. Defaults to "
|
|
111
|
+
"robot.action_spec.control_freq_hz or 30.0."
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Convert an mcap rosbag2 written by `Rosbag2Sink` into a LeRobotDataset v3.
|
|
116
|
+
|
|
117
|
+
Walks the bag's `/openral/episode` markers to segment episodes,
|
|
118
|
+
replays each tick through `RolloutRecorder` → `LeRobotDatasetSink`,
|
|
119
|
+
and produces an on-disk v3 dataset ready for `openral dataset push`.
|
|
120
|
+
|
|
121
|
+
Per CLAUDE.md §1.11 — this command uses real `mcap` and real
|
|
122
|
+
`lerobot.datasets.LeRobotDataset.create` end-to-end; no mocks.
|
|
123
|
+
"""
|
|
124
|
+
from openral_core import RobotDescription
|
|
125
|
+
from openral_core.exceptions import ROSError
|
|
126
|
+
from openral_dataset.converter import Rosbag2ToLeRobotConverter
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
robot = RobotDescription.from_yaml(str(robot_yaml))
|
|
130
|
+
except (OSError, ROSError) as exc:
|
|
131
|
+
_console.print(f"[red]robot.yaml error:[/red] {exc}")
|
|
132
|
+
raise typer.Exit(code=1) from exc
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
summary = Rosbag2ToLeRobotConverter.from_bag(
|
|
136
|
+
bag_path=bag_path,
|
|
137
|
+
robot=robot,
|
|
138
|
+
output_root=output_root,
|
|
139
|
+
repo_id=repo_id,
|
|
140
|
+
license=license_str,
|
|
141
|
+
fps=fps,
|
|
142
|
+
)
|
|
143
|
+
except ROSError as exc:
|
|
144
|
+
_console.print(f"[red]conversion error:[/red] {exc}")
|
|
145
|
+
raise typer.Exit(code=1) from exc
|
|
146
|
+
|
|
147
|
+
_console.print(
|
|
148
|
+
f"[green]converted:[/green] {summary.output_root}\n"
|
|
149
|
+
f" episodes : {summary.n_episodes} "
|
|
150
|
+
f"({summary.n_success} success / {summary.n_episodes - summary.n_success} failure)\n"
|
|
151
|
+
f" frames : {summary.n_frames}\n"
|
|
152
|
+
f" repo_id : {summary.repo_id}\n"
|
|
153
|
+
f" publish : openral dataset push {summary.output_root} --yes"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ── push subcommand ──────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
_CONSENT_PROMPT_BODY = """
|
|
161
|
+
You are about to upload a robot dataset to the Hugging Face Hub.
|
|
162
|
+
|
|
163
|
+
Dataset : {repo_id}
|
|
164
|
+
Local root : {root}
|
|
165
|
+
Visibility : PRIVATE (mandatory — OpenRAL never publishes public datasets automatically)
|
|
166
|
+
Episodes : {n_episodes}
|
|
167
|
+
Frames : {n_frames}
|
|
168
|
+
Cameras : {cameras}
|
|
169
|
+
License : {license}
|
|
170
|
+
|
|
171
|
+
This dataset may contain PERSONAL DATA:
|
|
172
|
+
- faces, bystanders, or other identifiable people in camera frames
|
|
173
|
+
- room layouts / posters / screens that disclose location or identity
|
|
174
|
+
- voice prints if audio sensors were recorded
|
|
175
|
+
- joint trajectories that may identify the operator (biometric)
|
|
176
|
+
|
|
177
|
+
You are responsible for:
|
|
178
|
+
- obtaining consent from anyone visible in camera frames
|
|
179
|
+
- the license you set ({license}) — PII-bearing data needs a stricter license
|
|
180
|
+
- regulatory compliance (GDPR / CCPA / equivalent) in your jurisdiction
|
|
181
|
+
|
|
182
|
+
The dataset's `meta/info.json` already records the license, episode count,
|
|
183
|
+
and success rate. The HF Hub repo will mirror them.
|
|
184
|
+
""".strip()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _read_info_json(root: Path) -> dict[str, object]:
|
|
188
|
+
"""Read ``meta/info.json`` from a local LeRobotDataset v3 root.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
ROSConfigError: When the file is missing or unparseable. A clean
|
|
192
|
+
``openral dataset push <root>`` failure here means the user
|
|
193
|
+
pointed at a non-dataset path; we surface that loudly.
|
|
194
|
+
"""
|
|
195
|
+
info_path = root / "meta" / "info.json"
|
|
196
|
+
if not info_path.is_file():
|
|
197
|
+
raise ROSConfigError(
|
|
198
|
+
f"{root} does not look like a LeRobotDataset v3 root "
|
|
199
|
+
f"(missing meta/info.json). "
|
|
200
|
+
"Did you mean to pass the directory written by "
|
|
201
|
+
"`openral sim run --dataset-out <path>`?"
|
|
202
|
+
)
|
|
203
|
+
try:
|
|
204
|
+
parsed = json.loads(info_path.read_text())
|
|
205
|
+
except json.JSONDecodeError as exc:
|
|
206
|
+
raise ROSConfigError(f"meta/info.json at {info_path} is not valid JSON: {exc!s}") from exc
|
|
207
|
+
if not isinstance(parsed, dict):
|
|
208
|
+
raise ROSConfigError(
|
|
209
|
+
f"meta/info.json at {info_path} is not a JSON object (got {type(parsed).__name__})"
|
|
210
|
+
)
|
|
211
|
+
return parsed
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _camera_keys_from_info(info: dict[str, object]) -> list[str]:
|
|
215
|
+
"""Extract ``observation.images.*`` feature keys from a v3 info.json."""
|
|
216
|
+
features = info.get("features", {})
|
|
217
|
+
if not isinstance(features, dict):
|
|
218
|
+
return []
|
|
219
|
+
return sorted(k for k in features if k.startswith("observation.images."))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _confirm_consent(repo_id: str, root: Path, info: dict[str, object], yes: bool) -> None:
|
|
223
|
+
"""Display the PII / consent disclosure and require an explicit confirmation.
|
|
224
|
+
|
|
225
|
+
Acceptable confirmations:
|
|
226
|
+
- ``--yes`` flag passed to ``push``
|
|
227
|
+
- ``OPENRAL_DATASET_CONSENT=1`` env var set
|
|
228
|
+
- Interactive prompt where the user retypes the repo_id
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
ROSConfigError: On any refusal / mismatch / non-interactive
|
|
232
|
+
invocation without the override.
|
|
233
|
+
"""
|
|
234
|
+
if yes:
|
|
235
|
+
_log.info("dataset.push.consent.flag_override", repo_id=repo_id)
|
|
236
|
+
return
|
|
237
|
+
if os.environ.get(_CONSENT_ENV_VAR) == "1":
|
|
238
|
+
_log.info("dataset.push.consent.env_override", repo_id=repo_id)
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
metadata = info.get("metadata", {})
|
|
242
|
+
license_str = (
|
|
243
|
+
metadata.get("license", "CC-BY-4.0") if isinstance(metadata, dict) else "CC-BY-4.0"
|
|
244
|
+
)
|
|
245
|
+
cameras = _camera_keys_from_info(info)
|
|
246
|
+
body = _CONSENT_PROMPT_BODY.format(
|
|
247
|
+
repo_id=repo_id,
|
|
248
|
+
root=str(root),
|
|
249
|
+
n_episodes=info.get("total_episodes", "?"),
|
|
250
|
+
n_frames=info.get("total_frames", "?"),
|
|
251
|
+
cameras=", ".join(cameras) or "(none)",
|
|
252
|
+
license=license_str,
|
|
253
|
+
)
|
|
254
|
+
_console.print(Panel(body, title="Consent required", border_style="yellow"))
|
|
255
|
+
|
|
256
|
+
if not sys.stdin.isatty():
|
|
257
|
+
raise ROSConfigError(
|
|
258
|
+
"non-interactive stdin: pass --yes or set OPENRAL_DATASET_CONSENT=1 "
|
|
259
|
+
"to skip the consent prompt in CI / scripted runs"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
prompt = f"Type the dataset repo_id ({repo_id!r}) to confirm:"
|
|
263
|
+
typed = typer.prompt(prompt, default="", show_default=False)
|
|
264
|
+
if typed.strip() != repo_id:
|
|
265
|
+
raise ROSConfigError(f"consent refused: typed {typed!r} did not match repo_id {repo_id!r}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _resolve_repo_id(root: Path, info: dict[str, object], cli_repo_id: str | None) -> str:
|
|
269
|
+
"""Resolve the target repo_id, preferring CLI override → meta/info.json → default.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
A non-empty ``<org>/<name>`` string. Raises if the resolved id
|
|
273
|
+
is malformed (missing ``/`` separator).
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ROSConfigError: When no repo_id can be resolved or the format
|
|
277
|
+
is invalid.
|
|
278
|
+
"""
|
|
279
|
+
if cli_repo_id is not None:
|
|
280
|
+
repo_id = cli_repo_id
|
|
281
|
+
else:
|
|
282
|
+
metadata = info.get("metadata", {})
|
|
283
|
+
if isinstance(metadata, dict) and metadata.get("repo_id"):
|
|
284
|
+
repo_id = str(metadata["repo_id"])
|
|
285
|
+
else:
|
|
286
|
+
raise ROSConfigError(
|
|
287
|
+
f"no repo_id found in {root}/meta/info.json[metadata][repo_id]; "
|
|
288
|
+
"pass --repo-id <org>/<name> on the command line"
|
|
289
|
+
)
|
|
290
|
+
if "/" not in repo_id or repo_id.startswith("/") or repo_id.endswith("/"):
|
|
291
|
+
raise ROSConfigError(f"invalid repo_id {repo_id!r}: must be of the form '<org>/<name>'")
|
|
292
|
+
return repo_id
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@dataset_app.command("push")
|
|
296
|
+
def push_command(
|
|
297
|
+
root: Path = typer.Argument(
|
|
298
|
+
...,
|
|
299
|
+
help=(
|
|
300
|
+
"Local LeRobotDataset v3 root directory. Must contain meta/info.json "
|
|
301
|
+
"(typically produced by `openral sim run --dataset-out <path>`)."
|
|
302
|
+
),
|
|
303
|
+
exists=True,
|
|
304
|
+
file_okay=False,
|
|
305
|
+
dir_okay=True,
|
|
306
|
+
resolve_path=True,
|
|
307
|
+
),
|
|
308
|
+
repo_id: str | None = typer.Option(
|
|
309
|
+
None,
|
|
310
|
+
"--repo-id",
|
|
311
|
+
help=(
|
|
312
|
+
"HF Hub repo id (e.g. openral/dataset-pick-cube). Overrides "
|
|
313
|
+
"meta/info.json[metadata][repo_id]; required when info.json "
|
|
314
|
+
"doesn't carry one."
|
|
315
|
+
),
|
|
316
|
+
),
|
|
317
|
+
yes: bool = typer.Option(
|
|
318
|
+
False,
|
|
319
|
+
"--yes",
|
|
320
|
+
"-y",
|
|
321
|
+
help=(
|
|
322
|
+
"Skip the interactive consent prompt. Required for non-TTY runs "
|
|
323
|
+
"(or set OPENRAL_DATASET_CONSENT=1 in the environment)."
|
|
324
|
+
),
|
|
325
|
+
),
|
|
326
|
+
dry_run: bool = typer.Option(
|
|
327
|
+
False,
|
|
328
|
+
"--dry-run",
|
|
329
|
+
help=(
|
|
330
|
+
"Validate the dataset and resolve the consent gate, but do "
|
|
331
|
+
"NOT contact the HF Hub. Useful for CI smoke checks."
|
|
332
|
+
),
|
|
333
|
+
),
|
|
334
|
+
token: str | None = typer.Option(
|
|
335
|
+
None,
|
|
336
|
+
"--token",
|
|
337
|
+
help=(
|
|
338
|
+
"HF token with 'repo.write' scope. Defaults to $HF_TOKEN / "
|
|
339
|
+
"$HUGGINGFACE_HUB_TOKEN env var."
|
|
340
|
+
),
|
|
341
|
+
),
|
|
342
|
+
commit_message: str | None = typer.Option(
|
|
343
|
+
None,
|
|
344
|
+
"--message",
|
|
345
|
+
"-m",
|
|
346
|
+
help="Commit message for the upload. Defaults to a structured auto-message.",
|
|
347
|
+
),
|
|
348
|
+
) -> None:
|
|
349
|
+
"""Upload a local LeRobotDataset v3 to the HF Hub (private; consent-gated).
|
|
350
|
+
|
|
351
|
+
Reads ``meta/info.json`` to determine the default repo_id and the
|
|
352
|
+
fields shown in the consent prompt (episode count, frame count,
|
|
353
|
+
camera keys, license). The local file system is the source of
|
|
354
|
+
truth — the HF Hub repo created here is a mirror.
|
|
355
|
+
|
|
356
|
+
The repo is **always** created with ``private=True``. A safety gate
|
|
357
|
+
re-fetches the repo metadata after creation and aborts if the API
|
|
358
|
+
reports it as public. The "discard vs persist + tag" decision is
|
|
359
|
+
deliberate; this command is the one place where user consent gates
|
|
360
|
+
whether dataset persistence becomes dataset publication.
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
info = _read_info_json(root)
|
|
364
|
+
resolved_repo_id = _resolve_repo_id(root, info, repo_id)
|
|
365
|
+
_confirm_consent(resolved_repo_id, root, info, yes=yes)
|
|
366
|
+
except ROSError as exc:
|
|
367
|
+
_console.print(f"[red]config error:[/red] {exc}")
|
|
368
|
+
raise typer.Exit(code=1) from exc
|
|
369
|
+
|
|
370
|
+
if dry_run:
|
|
371
|
+
_console.print(
|
|
372
|
+
f"[green]dry-run OK[/green] — would publish {resolved_repo_id!r} from {root}"
|
|
373
|
+
)
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
resolved_token = resolve_token(token)
|
|
378
|
+
except ROSError as exc:
|
|
379
|
+
_console.print(f"[red]config error:[/red] {exc}")
|
|
380
|
+
raise typer.Exit(code=1) from exc
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
from huggingface_hub import HfApi
|
|
384
|
+
except ImportError as exc:
|
|
385
|
+
_console.print(
|
|
386
|
+
"[red]config error:[/red] huggingface_hub is not installed. "
|
|
387
|
+
"Run `uv pip install huggingface_hub` and retry."
|
|
388
|
+
)
|
|
389
|
+
raise typer.Exit(code=1) from exc
|
|
390
|
+
|
|
391
|
+
api = HfApi(token=resolved_token)
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
repo_url = api.create_repo(
|
|
395
|
+
repo_id=resolved_repo_id,
|
|
396
|
+
repo_type="dataset",
|
|
397
|
+
private=True,
|
|
398
|
+
exist_ok=True,
|
|
399
|
+
)
|
|
400
|
+
except Exception as exc: # HF SDK raises many specific types
|
|
401
|
+
err_str = str(exc)
|
|
402
|
+
if "403" in err_str or "rights" in err_str.lower():
|
|
403
|
+
_console.print(
|
|
404
|
+
"[red]token insufficient:[/red] HF token does not have "
|
|
405
|
+
"'repo.write' scope. Regenerate with Write permission at "
|
|
406
|
+
"https://huggingface.co/settings/tokens."
|
|
407
|
+
)
|
|
408
|
+
else:
|
|
409
|
+
_console.print(f"[red]create_repo failed:[/red] {err_str}")
|
|
410
|
+
raise typer.Exit(code=1) from exc
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
ensure_private(api, resolved_repo_id, repo_type="dataset")
|
|
414
|
+
except ROSError as exc:
|
|
415
|
+
_console.print(f"[red]privacy gate:[/red] {exc}")
|
|
416
|
+
raise typer.Exit(code=1) from exc
|
|
417
|
+
|
|
418
|
+
message = commit_message or (
|
|
419
|
+
f"chore(dataset): publish {resolved_repo_id} "
|
|
420
|
+
f"({info.get('total_episodes', '?')} episodes, "
|
|
421
|
+
f"{info.get('total_frames', '?')} frames)"
|
|
422
|
+
)
|
|
423
|
+
try:
|
|
424
|
+
api.upload_folder(
|
|
425
|
+
folder_path=str(root),
|
|
426
|
+
repo_id=resolved_repo_id,
|
|
427
|
+
repo_type="dataset",
|
|
428
|
+
ignore_patterns=IGNORE_PATTERNS,
|
|
429
|
+
commit_message=message,
|
|
430
|
+
)
|
|
431
|
+
except Exception as exc:
|
|
432
|
+
_console.print(f"[red]upload failed:[/red] {exc}")
|
|
433
|
+
raise typer.Exit(code=1) from exc
|
|
434
|
+
|
|
435
|
+
_console.print(
|
|
436
|
+
f"[green]published:[/green] {repo_url} (private)\n"
|
|
437
|
+
f" load via: LeRobotDataset({resolved_repo_id!r})"
|
|
438
|
+
)
|