diff --git a/MANIFEST.in b/MANIFEST.in index 2dab1c90..21b77ff1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,20 +13,63 @@ include Makefile include requirements.txt include conversion_requirements.txt -# Include package data -recursive-include vla_arena *.yaml *.yml *.json *.txt -recursive-include vla_arena/vla_arena/assets * -recursive-include vla_arena/vla_arena/bddl_files *.bddl -recursive-include vla_arena/vla_arena/init_files *.pruned_init -recursive-include vla_arena/configs *.yaml +# Include package data - Python code only +recursive-include vla_arena *.py +recursive-include vla_arena/configs *.yaml *.yml *.json + +# IMPORTANT: Explicitly EXCLUDE large asset files from PyPI package +# These files are downloaded separately after installation +global-exclude *.obj +global-exclude *.stl +global-exclude *.dae +global-exclude *.mtl +global-exclude *.bddl +global-exclude *.pruned_init + +# Keep the placeholder files to preserve directory structure +global-include .gitkeep +include vla_arena/vla_arena/assets/.gitkeep +include vla_arena/vla_arena/bddl_files/.gitkeep +include vla_arena/vla_arena/init_files/.gitkeep + +# Explicitly exclude asset directories (belt and suspenders approach) +prune vla_arena/vla_arena/assets/stable_scanned_objects +prune vla_arena/vla_arena/assets/stable_hope_objects +prune vla_arena/vla_arena/assets/turbosquid_objects +prune vla_arena/vla_arena/assets/articulated_objects +prune vla_arena/vla_arena/assets/textures +prune vla_arena/vla_arena/bddl_files/generalization_object_preposition_combinations +prune vla_arena/vla_arena/bddl_files/generalization_task_workflows +prune vla_arena/vla_arena/bddl_files/generalization_unseen_objects +prune vla_arena/vla_arena/bddl_files/libero_10 +prune vla_arena/vla_arena/bddl_files/libero_90 +prune vla_arena/vla_arena/bddl_files/libero_goal +prune vla_arena/vla_arena/bddl_files/libero_object +prune vla_arena/vla_arena/bddl_files/libero_spatial +prune vla_arena/vla_arena/bddl_files/long_horizon +prune vla_arena/vla_arena/bddl_files/robustness_dynamic_distractors +prune vla_arena/vla_arena/bddl_files/robustness_static_distractors +prune vla_arena/vla_arena/bddl_files/safety_dynamic_obstacles +prune vla_arena/vla_arena/bddl_files/safety_static_obstacles +prune vla_arena/vla_arena/bddl_files/safety_hazard_avoidance +prune vla_arena/vla_arena/bddl_files/safety_risk_aware_grasping +prune vla_arena/vla_arena/bddl_files/safety_object_state_preservation +prune vla_arena/vla_arena/init_files + +# Include helper scripts +include scripts/download_tasks.py +include scripts/package_all_suites.py +include scripts/__init__.py +include scripts/manage_assets.py # Exclude unnecessary files recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-exclude * .DS_Store -recursive-exclude * .git* exclude .gitignore exclude .pre-commit-config.yaml +exclude .gitattributes +# Note: .gitkeep files are explicitly included above, not excluded # Exclude test and development files recursive-exclude tests * diff --git a/README.md b/README.md index d6a07a8a..fb52720e 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,21 @@ If you find VLA-Arena useful, please cite it in your publications. ## Quick Start ### 1. Installation + +#### Install from PyPI (Recommended) ```bash -# Clone repository +# 1. Install VLA-Arena +pip install vla-arena + +# 2. Download task suites (required) +vla-arena.download-tasks install-all --repo vla-arena/tasks +``` + +> **📦 Important**: To reduce PyPI package size, task suites and asset files must be downloaded separately after installation (~850 MB). + +#### Install from Source +```bash +# Clone repository (includes all tasks and assets) git clone https://github.com/PKU-Alignment/VLA-Arena.git cd VLA-Arena @@ -245,6 +258,61 @@ Fine-tune and evaluate VLA models using VLA-Arena generated datasets. - **English**: [`README_EN.md`](docs/README_EN.md) - Complete English documentation index - **中文**: [`README_ZH.md`](docs/README_ZH.md) - 完整中文文档索引 +### 📦 Download Task Suites + +#### Method 1: Using CLI Tool (Recommended) + +After installation, you can use the following commands to view and download task suites: + +```bash +# View installed tasks +vla-arena.download-tasks installed + +# List available task suites +vla-arena.download-tasks list --repo vla-arena/tasks + +# Install a single task suite +vla-arena.download-tasks install robustness_dynamic_distractors --repo vla-arena/tasks + +# Install all task suites (recommended) +vla-arena.download-tasks install-all --repo vla-arena/tasks +``` + +#### Method 2: Using Python Script + +```bash +# View installed tasks +python -m scripts.download_tasks installed + +# Install all tasks +python -m scripts.download_tasks install-all --repo vla-arena/tasks +``` + +### 🔧 Custom Task Repository + +If you want to use your own task repository: + +```bash +# Use custom HuggingFace repository +vla-arena.download-tasks install-all --repo your-username/your-task-repo +``` + +### 📝 Create and Share Custom Tasks + +You can create and share your own task suites: + +```bash +# Package a single task +vla-arena.manage-tasks pack path/to/task.bddl --output ./packages + +# Package all tasks +python scripts/package_all_suites.py --output ./packages + +# Upload to HuggingFace Hub +vla-arena.manage-tasks upload ./packages/my_task.vlap --repo your-username/your-repo +``` + + ## Leaderboard ### Performance Evaluation of VLA Models on the VLA-Arena Benchmark diff --git a/README_zh.md b/README_zh.md index b66006d1..839ab920 100644 --- a/README_zh.md +++ b/README_zh.md @@ -61,8 +61,21 @@ VLA-Arena 囊括四个任务类别: ## 快速开始 ### 1. 安装 + +#### 从 PyPI 安装 (推荐) ```bash -# 克隆仓库 +# 1. 安装 VLA-Arena +pip install vla-arena + +# 2. 下载任务套件 (必需) +vla-arena.download-tasks install-all --repo vla-arena/tasks +``` + +> **📦 重要**: 为减小 PyPI 包大小,任务套件和资产文件需要在安装后单独下载。 + +#### 从源代码安装 +```bash +# 克隆仓库(包含所有任务和资产文件) git clone https://github.com/PKU-Alignment/VLA-Arena.git cd VLA-Arena @@ -248,6 +261,60 @@ VLA-Arena为框架的所有方面提供全面的文档。选择最适合你需 - **中文**:[`README_ZH.md`](docs/README_ZH.md) - 完整中文文档索引 - **English**:[`README_EN.md`](docs/README_EN.md) - 完整英文文档索引 +### 📦 下载任务套件 + +#### 方法 1: 使用命令行工具 (推荐) + +安装后,你可以使用以下命令查看和下载任务套件: + +```bash +# 查看已安装的任务 +vla-arena.download-tasks installed + +# 列出可用的任务套件 +vla-arena.download-tasks list --repo vla-arena/tasks + +# 安装单个任务套件 +vla-arena.download-tasks install robustness_dynamic_distractors --repo vla-arena/tasks + +# 安装所有任务套件 (推荐) +vla-arena.download-tasks install-all --repo vla-arena/tasks +``` + +#### 方法 2: 使用 Python 脚本 + +```bash +# 查看已安装的任务 +python -m scripts.download_tasks installed + +# 安装所有任务 +python -m scripts.download_tasks install-all --repo vla-arena/tasks +``` + +### 🔧 自定义任务仓库 + +如果你想使用自己的任务仓库: + +```bash +# 使用自定义 HuggingFace 仓库 +vla-arena.download-tasks install-all --repo your-username/your-task-repo +``` + +### 📝 创建和分享自定义任务 + +你可以创建并分享自己的任务套件: + +```bash +# 打包单个任务 +vla-arena.manage-tasks pack path/to/task.bddl --output ./packages + +# 打包所有任务 +python scripts/package_all_suites.py --output ./packages + +# 上传到 HuggingFace Hub +vla-arena.manage-tasks upload ./packages/my_task.vlap --repo your-username/your-repo +``` + ## 排行榜 ### VLA模型在VLA-Arena基准测试上的性能评估 diff --git a/docs/asset_management.md b/docs/asset_management.md index dcc1a4f0..1b0e4d7a 100644 --- a/docs/asset_management.md +++ b/docs/asset_management.md @@ -139,7 +139,7 @@ python scripts/manage_assets.py upload \ ### Prerequisites 1. **HuggingFace Account**: Sign up at https://huggingface.co -2. **Create a Repository**: +2. **Create a Repository**: - Go to https://huggingface.co/new-dataset - Create a dataset repository (e.g., `username/vla-arena-tasks`) 3. **Get Access Token**: diff --git a/pyproject.toml b/pyproject.toml index b3594e0b..163bd4d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "vla-arena" -version = "0.1.0" +version = "1.0.0" authors = [ {name = "Jiahao Li", email = "jiahaoli2077@gmail.com"}, {name = "Borong Zhang"}, @@ -14,6 +14,13 @@ description = "VLA-Arena: A Comprehensive Benchmark for Vision-Language-Action M readme = "README.md" license = {text = "Apache-2.0"} requires-python = "==3.11" +keywords = ["Vision-Language-Action", "VLA Models", "Robotic Manipulation", "Benchmark"] +classifiers = [ + "Programming Language :: Python :: 3.11", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries :: Python Modules", +] dependencies = [ "imageio[ffmpeg]", @@ -36,8 +43,10 @@ dependencies = [ "vla-arena" = "vla_arena.cli.main:main" "vla-arena.main" = "vla_arena.main:main" "vla-arena.eval" = "vla_arena.evaluate:main" -"vla-arena.config_copy" = "scripts.config_copy:main" -"vla-arena.create_template" = "scripts.create_template:main" +"vla-arena.config-copy" = "scripts.config_copy:main" +"vla-arena.create-template" = "scripts.create_template:main" +"vla-arena.download-tasks" = "scripts.download_tasks:main" +"vla-arena.manage-tasks" = "scripts.manage_assets:main" [project.optional-dependencies] openvla = [ @@ -208,7 +217,7 @@ openpi = [ "polars>=1.30.0", ] -# Integrated Dev/Tool Dependencies from File B +# Integrated Dev/Tool Dependencies lint = [ "isort >= 5.11.0", "black >= 23.1.0", @@ -226,11 +235,13 @@ lint = [ "pyenchant", "pre-commit", ] + test = [ "pytest >= 7.0.0", "pytest-cov >= 3.0.0", "pytest-xdist >= 2.5.0", ] + docs = [ "sphinx >= 5.0.0", "sphinx-autoapi", @@ -240,16 +251,24 @@ docs = [ "myst-parser", ] -[tool.setuptools.packages.find] -where = ["."] -include = ["vla_arena*"] -exclude = [] +[project.urls] +Homepage = "https://vla-arena.github.io" +Repository = "https://github.com/PKU-Alignment/VLA-Arena" +Documentation = "https://github.com/PKU-Alignment/VLA-Arena/docs" +"Bug Report" = "https://github.com/PKU-Alignment/VLA-Arena/issues" [tool.setuptools] include-package-data = true eager-resources = ["*"] -# --- Tool Configurations (Integrated from File B) --- +[tool.setuptools.packages.find] +where = ["."] +include = ["vla_arena", "vla_arena.*", "scripts"] + +[tool.setuptools.dynamic] +version = {attr = "vla_arena.__version__"} + +# Linter tools ################################################################# [tool.black] line-length = 79 diff --git a/pytest.ini b/pytest.ini index d4722ee9..512376c8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,13 +5,14 @@ python_classes = Test* python_functions = test_* addopts = -v - --strict-markers --tb=short + --strict-markers + --disable-warnings markers = slow: marks tests as slow (deselect with '-m "not slow"') integration: marks tests as integration tests unit: marks tests as unit tests filterwarnings = - error + ignore::Warning ignore::DeprecationWarning ignore::PendingDeprecationWarning diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 570680d7..00000000 --- a/requirements.txt +++ /dev/null @@ -1,29 +0,0 @@ - setuptools==78.1.1 - hydra-core==1.2.0 - numpy==1.23.5 - wandb==0.13.1 - easydict==1.9 - opencv-python==4.6.0.66 - einops==0.4.1 - thop==0.1.1-2209072238 - robosuite==1.5.1 - bddl==1.0.1 - future==0.18.2 - matplotlib==3.5.3 - cloudpickle==2.1.0 - gym - tensorflow - IPython - timm==0.9.10 - transformers==4.40.1 - accelerate - imageio - imageio-ffmpeg - colorlog - rich - draccus - tensorflow_graphics - jsonlines - json_numpy - torch>=2.6.0 - dlimp @ git+https://github.com/moojink/dlimp_openvla diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 00000000..b096efe8 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Scripts package for VLA-Arena.""" diff --git a/scripts/download_tasks.py b/scripts/download_tasks.py new file mode 100644 index 00000000..d34f5b82 --- /dev/null +++ b/scripts/download_tasks.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +""" +VLA-Arena Task Suite Downloader + +Download and install task suites from HuggingFace Hub + +Usage: + # List available tasks + python scripts/download_tasks.py list --repo username/vla-arena-tasks + + # Download a single task suite + python scripts/download_tasks.py install robustness_dynamic_distractors --repo username/vla-arena-tasks + + # Download all task suites + python scripts/download_tasks.py install-all --repo username/vla-arena-tasks +""" + +import argparse +import os +import sys + + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from vla_arena.vla_arena.utils.asset_manager import TaskCloudManager, TaskInstaller + + +try: + from termcolor import colored +except ImportError: + + def colored(text, color=None, **kwargs): + return text + + +# Default official repository +DEFAULT_REPO = 'vla-arena/tasks' + + +def list_available_tasks(repo_id: str = DEFAULT_REPO): + """List available task suites""" + print(f'\nQuerying repository: {repo_id}\n') + + try: + cloud = TaskCloudManager(repo_id=repo_id) + packages = cloud.list_packages() + + if packages: + print(colored(f'✓ Found {len(packages)} task suites:\n', 'green')) + for i, pkg in enumerate(packages, 1): + print(f' {i:2d}. {pkg}') + print() + else: + print(colored('No task suites found', 'yellow')) + except Exception as e: + print(colored(f'❌ Error: {e}', 'red')) + return [] + + return packages + + +def install_task( + task_name: str, + repo_id: str = DEFAULT_REPO, + token: str = None, + overwrite: bool = False, + skip_existing_assets: bool = False, +): + """Download and install a single task suite""" + print(f'\nDownloading task suite: {task_name}') + print('=' * 80) + + try: + cloud = TaskCloudManager(repo_id=repo_id) + installer = TaskInstaller() + + # Download and install + success = cloud.download_and_install( + package_name=task_name, + overwrite=overwrite, + skip_existing_assets=skip_existing_assets, + token=token, + ) + + if success: + print(colored(f'\n✓ Task suite {task_name} installed successfully!', 'green')) + else: + print(colored(f'\n❌ Failed to install task suite {task_name}', 'red')) + + return success + + except Exception as e: + print(colored(f'\n❌ Error: {e}', 'red')) + return False + + +def install_all_tasks( + repo_id: str = DEFAULT_REPO, + token: str = None, + overwrite: bool = False, +): + """Download and install all task suites""" + print('\nDownloading all task suites') + print('=' * 80) + + # Get task list + packages = list_available_tasks(repo_id) + + if not packages: + print(colored('\nNo task suites available', 'yellow')) + return + + print(f'\nPreparing to install {len(packages)} task suites') + print('Note: Shared assets will be automatically skipped if already installed.\n') + + # Confirmation + response = input('Continue? [y/N]: ') + if response.lower() not in ['y', 'yes']: + print('Cancelled') + return + + # Install each task with skip_existing_assets=True to avoid conflicts + successful = [] + failed = [] + + for i, task_name in enumerate(packages, 1): + print(f'\n[{i}/{len(packages)}] Installing: {task_name}') + print('-' * 80) + + success = install_task( + task_name=task_name, + repo_id=repo_id, + token=token, + overwrite=overwrite, + skip_existing_assets=True, # Auto-skip shared assets + ) + + if success: + successful.append(task_name) + else: + failed.append(task_name) + + # Display statistics + print('\n' + '=' * 80) + print(f'\n✓ Installation complete: {len(successful)}/{len(packages)}') + + if successful: + print('\nSuccessfully installed:') + for task in successful: + print(f' ✓ {task}') + + if failed: + print('\nFailed to install:') + for task in failed: + print(f' ✗ {task}') + + +def get_installed_tasks(): + """Get list of installed task suites""" + from vla_arena.vla_arena import get_vla_arena_path + + bddl_root = get_vla_arena_path('bddl_files') + + installed = [] + if os.path.exists(bddl_root): + for item in os.listdir(bddl_root): + item_path = os.path.join(bddl_root, item) + if os.path.isdir(item_path) and not item.startswith('.'): + # Check if it has BDDL files + has_bddl = False + for root, dirs, files in os.walk(item_path): + if any(f.endswith('.bddl') for f in files): + has_bddl = True + break + if has_bddl: + installed.append(item) + + return sorted(installed) + + +def show_installed_tasks(): + """Display installed task suites""" + installed = get_installed_tasks() + + if installed: + print(colored(f'\n✓ {len(installed)} task suites installed:\n', 'green')) + for i, task in enumerate(installed, 1): + print(f' {i:2d}. {task}') + print() + else: + print(colored('\nNo task suites installed', 'yellow')) + print('\nUse the following command to install tasks:') + print(f' python scripts/download_tasks.py install-all --repo {DEFAULT_REPO}\n') + + +def main(): + parser = argparse.ArgumentParser( + description='VLA-Arena Task Suite Downloader', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # View installed tasks + python scripts/download_tasks.py installed + + # List available tasks + python scripts/download_tasks.py list --repo vla-arena/tasks + + # Install a single task + python scripts/download_tasks.py install robustness_dynamic_distractors --repo vla-arena/tasks + + # Install all tasks + python scripts/download_tasks.py install-all --repo vla-arena/tasks + """, + ) + + subparsers = parser.add_subparsers(dest='command', help='Commands') + + # list command + list_parser = subparsers.add_parser('list', help='List available task suites') + list_parser.add_argument( + '--repo', + default=DEFAULT_REPO, + help=f'HuggingFace repository ID (default: {DEFAULT_REPO})', + ) + + # installed command + subparsers.add_parser('installed', help='Show installed task suites') + + # install command + install_parser = subparsers.add_parser('install', help='Install a single task suite') + install_parser.add_argument('task_name', help='Task suite name') + install_parser.add_argument( + '--repo', + default=DEFAULT_REPO, + help=f'HuggingFace repository ID (default: {DEFAULT_REPO})', + ) + install_parser.add_argument('--token', help='HuggingFace API token') + install_parser.add_argument( + '--overwrite', + action='store_true', + help='Overwrite existing files', + ) + install_parser.add_argument( + '--skip-existing-assets', + action='store_true', + help='Skip assets that already exist (useful when installing multiple suites)', + ) + + # install-all command + install_all_parser = subparsers.add_parser('install-all', help='Install all task suites') + install_all_parser.add_argument( + '--repo', + default=DEFAULT_REPO, + help=f'HuggingFace repository ID (default: {DEFAULT_REPO})', + ) + install_all_parser.add_argument('--token', help='HuggingFace API token') + install_all_parser.add_argument( + '--overwrite', + action='store_true', + help='Overwrite existing files', + ) + + args = parser.parse_args() + + if args.command == 'list': + list_available_tasks(repo_id=args.repo) + + elif args.command == 'installed': + show_installed_tasks() + + elif args.command == 'install': + install_task( + task_name=args.task_name, + repo_id=args.repo, + token=args.token, + overwrite=args.overwrite, + skip_existing_assets=getattr(args, 'skip_existing_assets', False), + ) + + elif args.command == 'install-all': + install_all_tasks( + repo_id=args.repo, + token=args.token, + overwrite=args.overwrite, + ) + + else: + parser.print_help() + + +if __name__ == '__main__': + main() diff --git a/scripts/package_all_suites.py b/scripts/package_all_suites.py new file mode 100644 index 00000000..7cb20f57 --- /dev/null +++ b/scripts/package_all_suites.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +""" +Batch package all task suites into .vlap packages + +Usage: + python scripts/package_all_suites.py --output ./packages --author "VLA-Arena Team" + +Optional arguments: + --upload: Automatically upload to HuggingFace Hub after packaging + --repo: HuggingFace repository ID (if using --upload) + --token: HuggingFace API token (if using --upload) +""" + +import argparse +import os +import sys + + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from vla_arena.vla_arena.utils.asset_manager import ( + TaskCloudManager, + TaskPackager, + get_vla_arena_path, +) + + +def get_all_task_suites(): + """Get names of all task suites""" + bddl_root = get_vla_arena_path('bddl_files') + suites = [] + + for item in os.listdir(bddl_root): + item_path = os.path.join(bddl_root, item) + if os.path.isdir(item_path) and not item.startswith('.'): + suites.append(item) + + return sorted(suites) + + +def package_all_suites( + output_dir: str, + author: str = 'VLA-Arena Team', + email: str = '', + upload: bool = False, + repo_id: str = None, + token: str = None, +): + """ + Package all task suites + + Args: + output_dir: Output directory + author: Author name + email: Author email + upload: Whether to upload to HuggingFace Hub + repo_id: HuggingFace repository ID + token: HuggingFace API token + """ + os.makedirs(output_dir, exist_ok=True) + + # Get all task suites + suites = get_all_task_suites() + + print(f'\nFound {len(suites)} task suites:\n') + for i, suite in enumerate(suites, 1): + print(f' {i}. {suite}') + + print('\nStarting packaging...\n') + print('=' * 80) + + packager = TaskPackager() + packaged_files = [] + + # Package each suite + for i, suite_name in enumerate(suites, 1): + print(f'\n[{i}/{len(suites)}] Packaging: {suite_name}') + print('-' * 80) + + try: + # Generate description + description = f'{suite_name} task suite from VLA-Arena benchmark' + + # Package + package_path = packager.pack_task_suite( + task_suite_name=suite_name, + output_dir=output_dir, + author=author, + email=email, + description=description, + ) + + packaged_files.append((suite_name, package_path)) + + except Exception as e: + print(f' ❌ Error: {e}') + continue + + print('\n' + '=' * 80) + print(f'\n✓ Packaging complete! {len(packaged_files)}/{len(suites)} suites') + print(f'\nPackages saved to: {output_dir}\n') + + # Display packaging statistics + total_size = 0 + for suite_name, package_path in packaged_files: + if os.path.exists(package_path): + size = os.path.getsize(package_path) + total_size += size + print(f' - {suite_name}: {size / 1024 / 1024:.2f} MB') + + print(f'\nTotal size: {total_size / 1024 / 1024:.2f} MB') + + # Upload to HuggingFace Hub + if upload: + if not repo_id: + print('\n⚠ Need to provide --repo parameter to upload to HuggingFace Hub') + return packaged_files + + print(f'\nUploading to HuggingFace Hub: {repo_id}') + print('=' * 80) + + cloud = TaskCloudManager(repo_id=repo_id) + + for i, (suite_name, package_path) in enumerate(packaged_files, 1): + print(f'\n[{i}/{len(packaged_files)}] Uploading: {suite_name}') + print('-' * 80) + + try: + url = cloud.upload( + package_path=package_path, + token=token, + private=False, + ) + print(f' ✓ Upload successful: {url}') + except Exception as e: + print(f' ❌ Upload failed: {e}') + + print('\n' + '=' * 80) + print('\n✓ Upload complete!') + + return packaged_files + + +def main(): + parser = argparse.ArgumentParser( + description='Batch package all VLA-Arena task suites', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Package only + python scripts/package_all_suites.py --output ./packages + + # Package and upload + python scripts/package_all_suites.py --output ./packages \\ + --upload --repo username/vla-arena-tasks --token hf_xxx + """, + ) + + parser.add_argument( + '--output', + '-o', + default='./packages', + help='Output directory (default: ./packages)', + ) + parser.add_argument( + '--author', + default='VLA-Arena Team', + help='Author name', + ) + parser.add_argument( + '--email', + default='', + help='Author email', + ) + parser.add_argument( + '--upload', + action='store_true', + help='Automatically upload to HuggingFace Hub after packaging', + ) + parser.add_argument( + '--repo', + help='HuggingFace repository ID (e.g., username/vla-arena-tasks)', + ) + parser.add_argument( + '--token', + help='HuggingFace API token', + ) + + args = parser.parse_args() + + package_all_suites( + output_dir=args.output, + author=args.author, + email=args.email, + upload=args.upload, + repo_id=args.repo, + token=args.token, + ) + + +if __name__ == '__main__': + main() diff --git a/tests/README.md b/tests/README.md index edb773a8..5e8d5ccf 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,16 @@ -# VLA-Arena Test Suite +# VLA-Arena Tests This directory contains comprehensive pytest tests for the VLA-Arena project. +## Running Tests + +### Install test dependencies + +```bash +pip install pytest pytest-cov +``` + + ## Test Structure - `conftest.py` - Pytest configuration and shared fixtures @@ -34,22 +43,65 @@ pytest tests/test_utils.py pytest tests/ --cov=vla_arena --cov-report=html ``` -### Run only unit tests (exclude integration tests) +### Run with verbose output ```bash -pytest tests/ -m "not integration" +# Run all tests +pytest + +# Run with coverage +pytest --cov=vla_arena --cov-report=html + +# Run specific test file +pytest tests/test_asset_packaging.py + +# Run specific test +pytest tests/test_asset_packaging.py::TestTaskPackaging::test_pack_single_task ``` -### Run only fast tests (exclude slow tests) +### Test markers ```bash -pytest tests/ -m "not slow" +# Run only unit tests +pytest -m unit + +# Run only integration tests +pytest -m integration + +# Skip slow tests +pytest -m "not slow" ``` -### Run with verbose output +## Test Structure -```bash -pytest tests/ -v +``` +tests/ +├── __init__.py +├── README.md +└── test_asset_packaging.py # Asset packaging and installation tests +``` + +## Writing Tests + +Example test: + +```python +import pytest +from vla_arena.vla_arena.utils.asset_manager import TaskPackager + +def test_pack_task(): + packager = TaskPackager() + # ... test code +``` + +## CI/CD Integration + +Tests can be integrated into CI/CD pipelines: + +```yaml +# .github/workflows/test.yml +- name: Run tests + run: pytest --cov=vla_arena ``` ## Test Markers diff --git a/tests/conftest.py b/tests/conftest.py index 0ececee5..37c70a91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -106,13 +106,18 @@ def mock_benchmark(): @pytest.fixture(autouse=True) def reset_benchmark_registry(): """Reset benchmark registry before each test.""" - from vla_arena.vla_arena.benchmark import BENCHMARK_MAPPING - - original_mapping = BENCHMARK_MAPPING.copy() - BENCHMARK_MAPPING.clear() - yield - BENCHMARK_MAPPING.clear() - BENCHMARK_MAPPING.update(original_mapping) + try: + from vla_arena.vla_arena.benchmark import BENCHMARK_MAPPING + + original_mapping = BENCHMARK_MAPPING.copy() + BENCHMARK_MAPPING.clear() + yield + BENCHMARK_MAPPING.clear() + BENCHMARK_MAPPING.update(original_mapping) + except (ImportError, RuntimeError) as e: + # Skip if robosuite or other dependencies are not available + pytest.skip(f'Skipping benchmark registry reset: {e}') + yield @pytest.fixture diff --git a/tests/test_asset_packaging.py b/tests/test_asset_packaging.py new file mode 100644 index 00000000..45398451 --- /dev/null +++ b/tests/test_asset_packaging.py @@ -0,0 +1,258 @@ +""" +Test asset packaging workflow + +Tests the complete workflow: +1. Pack individual task +2. Pack task suite +3. Inspect package +4. Install package +5. Uninstall package +""" + +import os +import shutil +import tempfile + +import pytest + +from vla_arena.vla_arena import get_vla_arena_path +from vla_arena.vla_arena.utils.asset_manager import TaskInstaller, TaskPackager + + +@pytest.fixture +def temp_output_dir(): + """Create a temporary directory for test outputs""" + temp_dir = tempfile.mkdtemp(prefix='vla_arena_test_') + yield temp_dir + # Cleanup + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + + +@pytest.fixture +def sample_bddl_file(): + """Get a sample BDDL file for testing""" + bddl_root = get_vla_arena_path('bddl_files') + + # Find first available BDDL file + for suite_dir in os.listdir(bddl_root): + suite_path = os.path.join(bddl_root, suite_dir) + if os.path.isdir(suite_path) and not suite_dir.startswith('.'): + for root, dirs, files in os.walk(suite_path): + bddl_files = [f for f in files if f.endswith('.bddl')] + if bddl_files: + return os.path.join(root, bddl_files[0]) + + pytest.skip('No BDDL files found') + + +class TestTaskPackaging: + """Test task packaging functionality""" + + def test_pack_single_task(self, temp_output_dir, sample_bddl_file): + """Test packing a single task""" + packager = TaskPackager() + + package_path = packager.pack( + bddl_path=sample_bddl_file, + output_dir=temp_output_dir, + author='Test User', + description='Test package', + ) + + # Verify package was created + assert os.path.exists(package_path) + assert package_path.endswith('.vlap') + + # Verify package size is reasonable (should be < 100MB for a single task) + size_mb = os.path.getsize(package_path) / 1024 / 1024 + assert size_mb < 100, f'Package too large: {size_mb:.2f} MB' + + def test_pack_task_suite(self, temp_output_dir): + """Test packing a task suite""" + packager = TaskPackager() + + # Get first available task suite + bddl_root = get_vla_arena_path('bddl_files') + suite_name = None + for item in os.listdir(bddl_root): + if os.path.isdir( + os.path.join(bddl_root, item), + ) and not item.startswith('.'): + suite_name = item + break + + if not suite_name: + pytest.skip('No task suites found') + + package_path = packager.pack_task_suite( + task_suite_name=suite_name, + output_dir=temp_output_dir, + author='Test User', + ) + + # Verify package was created + assert os.path.exists(package_path) + assert suite_name in package_path + + def test_inspect_package(self, temp_output_dir, sample_bddl_file): + """Test inspecting a package""" + packager = TaskPackager() + installer = TaskInstaller() + + # Create a package + package_path = packager.pack( + bddl_path=sample_bddl_file, + output_dir=temp_output_dir, + author='Test User', + ) + + # Inspect the package + manifest = installer.inspect(package_path) + + # Verify manifest contents + assert manifest.package_name + assert manifest.author == 'Test User' + assert len(manifest.bddl_files) > 0 + assert len(manifest.assets) >= 0 # May be 0 if no assets + + def test_conflict_detection(self, temp_output_dir, sample_bddl_file): + """Test conflict detection""" + packager = TaskPackager() + installer = TaskInstaller() + + # Create a package + package_path = packager.pack( + bddl_path=sample_bddl_file, + output_dir=temp_output_dir, + author='Test User', + ) + + # Check for conflicts (should find some if assets already installed) + conflicts = installer.check_conflicts(package_path) + + # Verify conflict detection structure + assert 'assets' in conflicts + assert 'bddl_files' in conflicts + assert 'init_files' in conflicts + assert isinstance(conflicts['assets'], list) + + def test_dry_run_install(self, temp_output_dir, sample_bddl_file): + """Test dry-run installation""" + packager = TaskPackager() + installer = TaskInstaller() + + # Create a package + package_path = packager.pack( + bddl_path=sample_bddl_file, + output_dir=temp_output_dir, + author='Test User', + ) + + # Dry run with skip_existing_assets should always succeed + result = installer.install( + package_path, + dry_run=True, + skip_existing_assets=True, + ) + assert result is True + + +class TestAssetManager: + """Test asset manager functionality""" + + def test_package_format(self, temp_output_dir, sample_bddl_file): + """Test that package format is correct""" + import zipfile + + packager = TaskPackager() + package_path = packager.pack( + bddl_path=sample_bddl_file, + output_dir=temp_output_dir, + author='Test User', + ) + + # Verify it's a valid ZIP file + assert zipfile.is_zipfile(package_path) + + # Verify manifest exists + with zipfile.ZipFile(package_path, 'r') as zf: + files = zf.namelist() + manifest_files = [f for f in files if 'manifest.json' in f] + assert ( + len(manifest_files) > 0 + ), 'manifest.json not found in package' + + def test_package_contains_bddl(self, temp_output_dir, sample_bddl_file): + """Test that package contains BDDL files""" + import zipfile + + packager = TaskPackager() + package_path = packager.pack( + bddl_path=sample_bddl_file, + output_dir=temp_output_dir, + author='Test User', + ) + + # Verify BDDL files are included + with zipfile.ZipFile(package_path, 'r') as zf: + files = zf.namelist() + bddl_files = [f for f in files if f.endswith('.bddl')] + assert len(bddl_files) > 0, 'No BDDL files found in package' + + def test_skip_existing_assets(self, temp_output_dir, sample_bddl_file): + """Test skip_existing_assets functionality""" + packager = TaskPackager() + installer = TaskInstaller() + + # Create a package + package_path = packager.pack( + bddl_path=sample_bddl_file, + output_dir=temp_output_dir, + author='Test User', + ) + + # Test with skip_existing_assets=True should not fail on conflicts + result = installer.install( + package_path, + skip_existing_assets=True, + dry_run=True, + ) + assert result is True + + +class TestPathResolution: + """Test path resolution functionality""" + + def test_get_vla_arena_path(self): + """Test VLA-Arena path resolution""" + # Test all standard paths + paths_to_test = [ + 'assets', + 'bddl_files', + 'init_states', + 'benchmark_root', + ] + + for key in paths_to_test: + path = get_vla_arena_path(key) + assert path is not None + assert isinstance(path, str) + # Path should be absolute + assert os.path.isabs(path) + + def test_assets_directory_exists(self): + """Test that assets directory exists""" + assets_path = get_vla_arena_path('assets') + assert os.path.exists(assets_path) + assert os.path.isdir(assets_path) + + def test_bddl_directory_exists(self): + """Test that BDDL directory exists""" + bddl_path = get_vla_arena_path('bddl_files') + assert os.path.exists(bddl_path) + assert os.path.isdir(bddl_path) + + +if __name__ == '__main__': + pytest.main([__file__, '-v']) diff --git a/vla_arena/configs/evaluation/openvla.yaml b/vla_arena/configs/evaluation/openvla.yaml index 05538f0e..e54bf954 100644 --- a/vla_arena/configs/evaluation/openvla.yaml +++ b/vla_arena/configs/evaluation/openvla.yaml @@ -3,12 +3,12 @@ pretrained_checkpoint: "your-openvla-checkpoint" center_crop: true # Center crop? (if trained with random crop augmentation) num_open_loop_steps: 8 # Open-loop steps before requerying policy - unnorm_key: "libero_spatial" # Action un-normalization key + unnorm_key: "vla_arena" # Action un-normalization key load_in_8bit: false # Load with 8-bit quantization load_in_4bit: false # Load with 4-bit quantization seed: 7 # Random seed for reproducibility - task_suite_name: "libero_spatial" # Task suite name + task_suite_name: "safety_static_obstacles" # Task suite name task_level: 0 num_steps_wait: 10 # Steps to wait for objects to stabilize num_trials_per_task: 10 # Rollouts per task diff --git a/vla_arena/configs/evaluation/openvla_oft.yaml b/vla_arena/configs/evaluation/openvla_oft.yaml index bae7ce9b..991d7adb 100644 --- a/vla_arena/configs/evaluation/openvla_oft.yaml +++ b/vla_arena/configs/evaluation/openvla_oft.yaml @@ -16,13 +16,13 @@ num_open_loop_steps: 8 # Number of actions to execute open-lo lora_rank: 32 # Rank of LoRA weight matrix (MAKE SURE THIS MATCHES TRAINING!) -unnorm_key: "libero_spatial" # Action un-normalization key +unnorm_key: "vla_arena" # Action un-normalization key load_in_8bit: false # (For OpenVLA only) Load with 8-bit quantization load_in_4bit: false # (For OpenVLA only) Load with 4-bit quantization # VLA-Arena environment-specific parameters -task_suite_name: "libero_spatial" # Task suite +task_suite_name: "safety_static_obstacles" # Task suite task_level: 0 num_steps_wait: 10 # Number of steps to wait for objects to stabilize in sim num_trials_per_task: 10 # Number of rollouts per task diff --git a/vla_arena/configs/train/openvla.yaml b/vla_arena/configs/train/openvla.yaml index fdb0b997..6ccad3ad 100644 --- a/vla_arena/configs/train/openvla.yaml +++ b/vla_arena/configs/train/openvla.yaml @@ -1,5 +1,5 @@ # Set OPENVLA_VLA_PATH environment variable or modify this path to specify your OpenVLA model location -vla_path: "/path/to/your/openvla-model" # Path to OpenVLA model (on HuggingFace Hub) +vla_path: /path/to/your/openvla-model # Path to OpenVLA model # Directory Paths # Set OPENVLA_DATA_ROOT_DIR environment variable or modify this path to specify your dataset directory @@ -26,5 +26,5 @@ use_quantization: false # Whether to 4-bit quantize VLA # Tracking Parameters wandb_project: "openvla" # Name of W&B project to log to (use default!) -wandb_entity: "stanford-voltron" # Name of entity to log under +wandb_entity: "your_wandb_entity" # Name of entity to log under run_id_note: null # Extra note for logging, Weights & Biases diff --git a/vla_arena/configs/train/openvla_oft.yaml b/vla_arena/configs/train/openvla_oft.yaml index 65b19240..9c40be6b 100644 --- a/vla_arena/configs/train/openvla_oft.yaml +++ b/vla_arena/configs/train/openvla_oft.yaml @@ -1,11 +1,11 @@ # Model Path # Set OPENVLA_OFT_VLA_PATH environment variable or modify this path to specify your OpenVLA model location -vla_path: "/path/to/your/models/openvla" # Path to OpenVLA model (on HuggingFace Hub or stored locally) +vla_path: /path/to/your/models/openvla # Path to OpenVLA model (on HuggingFace Hub or stored locally) # Dataset # Set OPENVLA_OFT_DATA_ROOT_DIR environment variable or modify this path to specify your dataset directory data_root_dir: "/path/to/your/datasets/openvla_spatial" # Directory containing RLDS datasets -dataset_name: "libero_spatial_no_noops" # Name of fine-tuning dataset (e.g., `aloha_scoop_x_into_bowl`) +dataset_name: "vla_arena" # Name of fine-tuning dataset (e.g., `aloha_scoop_x_into_bowl`) run_root_dir: "runs" # Path to directory to store logs & checkpoints shuffle_buffer_size: 100000 # Dataloader shuffle buffer size (can reduce if OOM errors occur) diff --git a/vla_arena/models/openpi/packages/openpi-client/src/openpi_client/__init__.py b/vla_arena/models/openpi/packages/openpi-client/src/openpi_client/__init__.py index 1ca9cd2f..2be5245e 100644 --- a/vla_arena/models/openpi/packages/openpi-client/src/openpi_client/__init__.py +++ b/vla_arena/models/openpi/packages/openpi-client/src/openpi_client/__init__.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '0.1.0' +__version__ = '1.0.0' diff --git a/vla_arena/models/smolvla/tests/artifacts/image_transforms/single_transforms.safetensors b/vla_arena/models/smolvla/tests/artifacts/image_transforms/single_transforms.safetensors deleted file mode 100644 index 7a0599d9..00000000 --- a/vla_arena/models/smolvla/tests/artifacts/image_transforms/single_transforms.safetensors +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d4ebab73eabddc58879a4e770289d19e00a1a4cf2fa5fa33cd3a3246992bc90 -size 40551392 diff --git a/vla_arena/vla_arena/__init__.py b/vla_arena/vla_arena/__init__.py index 44f239a6..9b14c192 100644 --- a/vla_arena/vla_arena/__init__.py +++ b/vla_arena/vla_arena/__init__.py @@ -40,7 +40,7 @@ def get_vla_arena_path(query_key): if query_key not in paths: raise KeyError( - f"Key '{query_key}' not found. Available keys: {list(paths.keys())}" + f"Key '{query_key}' not found. Available keys: {list(paths.keys())}", ) return os.path.abspath(paths[query_key]) diff --git a/vla_arena/vla_arena/assets/.gitkeep b/vla_arena/vla_arena/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/vla_arena/vla_arena/bddl_files/.gitkeep b/vla_arena/vla_arena/bddl_files/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/vla_arena/vla_arena/init_files/.gitkeep b/vla_arena/vla_arena/init_files/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/vla_arena/vla_arena/utils/asset_manager.py b/vla_arena/vla_arena/utils/asset_manager.py index 12ddf243..c083d0d2 100644 --- a/vla_arena/vla_arena/utils/asset_manager.py +++ b/vla_arena/vla_arena/utils/asset_manager.py @@ -141,7 +141,9 @@ def lightweight_parse_bddl(bddl_path: str) -> dict: # Extract objects of interest interest_match = re.search( - r'\(:obj_of_interest([^)]*)\)', content, re.DOTALL + r'\(:obj_of_interest([^)]*)\)', + content, + re.DOTALL, ) if interest_match: interest_content = interest_match.group(1).strip() @@ -162,7 +164,9 @@ def find_problem_class_file(problem_name: str) -> str | None: """ try: problems_dir = os.path.join( - get_vla_arena_path('benchmark_root'), 'envs', 'problems' + get_vla_arena_path('benchmark_root'), + 'envs', + 'problems', ) except: return None @@ -271,7 +275,7 @@ def parse_scene_xml_assets(scene_xml_path: str) -> dict[str, list[str]]: for match in re.finditer(pattern, content): mesh_path = match.group(1) full_path = os.path.normpath( - os.path.join(scene_dir, mesh_path) + os.path.join(scene_dir, mesh_path), ) if os.path.exists(full_path): result['meshes'].append(full_path) @@ -437,11 +441,11 @@ class TaskManifest: bddl_files: list[str] = field(default_factory=list) init_files: list[str] = field(default_factory=list) problem_files: list[str] = field( - default_factory=list + default_factory=list, ) # Custom Problem class files scene_files: list[str] = field(default_factory=list) # Scene XML files scene_assets: list[str] = field( - default_factory=list + default_factory=list, ) # Scene textures/meshes assets: list[dict] = field(default_factory=list) # Object assets @@ -614,7 +618,9 @@ def get_asset_paths(self, object_names: set[str]) -> list[AssetInfo]: 'articulated_objects', ]: asset_path = os.path.join( - assets_root, source_dir, name_lower + assets_root, + source_dir, + name_lower, ) if os.path.exists(asset_path): category = { @@ -725,7 +731,9 @@ def _find_init_file_for_bddl(self, bddl_path: str) -> str | None: # Construct potential init file path init_file = os.path.join( - init_root, rel_dir, f'{bddl_name}.pruned_init' + init_root, + rel_dir, + f'{bddl_name}.pruned_init', ) if os.path.exists(init_file): @@ -803,7 +811,7 @@ def pack( if include_problem and scene_info.problem_file: problem_files_list.append( - os.path.basename(scene_info.problem_file) + os.path.basename(scene_info.problem_file), ) if include_scene and scene_info.scene_xml: @@ -826,7 +834,7 @@ def pack( package_name=package_name, task_name=parsed.get('problem_name', ''), language_instruction=' '.join( - parsed.get('language_instruction', []) + parsed.get('language_instruction', []), ), description=description, problem_class=scene_info.problem_name, @@ -846,7 +854,8 @@ def pack( # Create package os.makedirs(output_dir, exist_ok=True) package_path = os.path.join( - output_dir, f'{package_name}{PACKAGE_EXTENSION}' + output_dir, + f'{package_name}{PACKAGE_EXTENSION}', ) with tempfile.TemporaryDirectory() as temp_dir: @@ -871,7 +880,7 @@ def pack( os.makedirs(problems_dir, exist_ok=True) shutil.copy2(scene_info.problem_file, problems_dir) print( - f' + Problem class: {os.path.basename(scene_info.problem_file)}' + f' + Problem class: {os.path.basename(scene_info.problem_file)}', ) # Copy scene XML and its assets @@ -881,7 +890,7 @@ def pack( # Copy scene XML (preserve directory structure under assets/) scene_rel_dir = os.path.dirname( - scene_info.scene_xml + scene_info.scene_xml, ) # e.g., "scenes" scene_dest_dir = os.path.join(assets_dir, scene_rel_dir) os.makedirs(scene_dest_dir, exist_ok=True) @@ -922,7 +931,8 @@ def pack( category_dir = os.path.join( assets_dir, ASSET_SOURCE_MAPPING.get( - asset.category, asset.category + asset.category, + asset.category, ), ) os.makedirs(category_dir, exist_ok=True) @@ -937,7 +947,9 @@ def pack( # Create ZIP archive with zipfile.ZipFile( - package_path, 'w', zipfile.ZIP_DEFLATED + package_path, + 'w', + zipfile.ZIP_DEFLATED, ) as zf: for root, dirs, files in os.walk(pkg_root): for file in files: @@ -999,7 +1011,7 @@ def pack_task_suite( if f.endswith('.bddl'): bddl_path = os.path.join(root, f) fixtures, objects, _ = self.analyzer.analyze_bddl( - bddl_path + bddl_path, ) all_fixtures |= fixtures all_objects |= objects @@ -1012,7 +1024,8 @@ def pack_task_suite( for f in files: if f.endswith('.pruned_init'): rel_path = os.path.relpath( - os.path.join(root, f), suite_init_dir + os.path.join(root, f), + suite_init_dir, ) init_files.append(rel_path) @@ -1038,7 +1051,8 @@ def pack_task_suite( # Create package os.makedirs(output_dir, exist_ok=True) package_path = os.path.join( - output_dir, f'{task_suite_name}{PACKAGE_EXTENSION}' + output_dir, + f'{task_suite_name}{PACKAGE_EXTENSION}', ) with tempfile.TemporaryDirectory() as temp_dir: @@ -1073,7 +1087,9 @@ def pack_task_suite( # Create ZIP with zipfile.ZipFile( - package_path, 'w', zipfile.ZIP_DEFLATED + package_path, + 'w', + zipfile.ZIP_DEFLATED, ) as zf: for root, dirs, files in os.walk(pkg_root): for file in files: @@ -1149,10 +1165,13 @@ def check_conflicts(self, package_path: str) -> dict[str, list[str]]: for asset in manifest.assets: asset_info = AssetInfo(**asset) category_dir = ASSET_SOURCE_MAPPING.get( - asset_info.category, asset_info.category + asset_info.category, + asset_info.category, ) existing_path = os.path.join( - self.assets_root, category_dir, asset_info.name + self.assets_root, + category_dir, + asset_info.name, ) if os.path.exists(existing_path): # Store relative path from assets root @@ -1162,7 +1181,9 @@ def check_conflicts(self, package_path: str) -> dict[str, list[str]]: # Check BDDL files for bddl_file in manifest.bddl_files: existing_path = os.path.join( - self.bddl_root, manifest.package_name, bddl_file + self.bddl_root, + manifest.package_name, + bddl_file, ) if os.path.exists(existing_path): conflicts['bddl_files'].append(bddl_file) @@ -1170,7 +1191,9 @@ def check_conflicts(self, package_path: str) -> dict[str, list[str]]: # Check init files for init_file in manifest.init_files: existing_path = os.path.join( - self.init_root, manifest.package_name, init_file + self.init_root, + manifest.package_name, + init_file, ) if os.path.exists(existing_path): conflicts['init_files'].append(init_file) @@ -1182,6 +1205,7 @@ def install( package_path: str, overwrite: bool = False, skip_assets: bool = False, + skip_existing_assets: bool = False, dry_run: bool = False, ) -> bool: """ @@ -1191,6 +1215,7 @@ def install( package_path: Path to the .vlap package overwrite: Whether to overwrite existing files skip_assets: Skip installing assets (useful if already installed) + skip_existing_assets: Skip existing assets but install new ones (useful for install-all) dry_run: Only show what would be installed Returns: @@ -1199,7 +1224,7 @@ def install( manifest = self.inspect(package_path) print( - f"\n{'[DRY RUN] ' if dry_run else ''}Installing: {manifest.package_name}" + f"\n{'[DRY RUN] ' if dry_run else ''}Installing: {manifest.package_name}", ) print(f' Task: {manifest.task_name}') print(f' Description: {manifest.description}') @@ -1207,7 +1232,17 @@ def install( # Check conflicts conflicts = self.check_conflicts(package_path) - has_conflicts = any(len(v) > 0 for v in conflicts.values()) + + # If skip_existing_assets is True, ignore asset conflicts + if skip_existing_assets: + conflicts_to_check = { + 'bddl_files': conflicts['bddl_files'], + 'init_files': conflicts['init_files'], + } + else: + conflicts_to_check = conflicts + + has_conflicts = any(len(v) > 0 for v in conflicts_to_check.values()) if has_conflicts and not overwrite: print(colored('\n⚠ Conflicts detected:', 'yellow')) @@ -1218,7 +1253,7 @@ def install( print(f' - BDDL: {bddl_file} (already exists)') if len(conflicts['bddl_files']) > 5: print( - f" - ... and {len(conflicts['bddl_files'])-5} more BDDL files" + f" - ... and {len(conflicts['bddl_files'])-5} more BDDL files", ) # Display init file conflicts @@ -1227,7 +1262,7 @@ def install( print(f' - Init: {init_file} (already exists)') if len(conflicts['init_files']) > 5: print( - f" - ... and {len(conflicts['init_files'])-5} more init files" + f" - ... and {len(conflicts['init_files'])-5} more init files", ) # Display asset conflicts @@ -1237,21 +1272,23 @@ def install( if len(conflicts['assets']) > 5: print( - f" - ... and {len(conflicts['assets'])-5} more assets" + f" - ... and {len(conflicts['assets'])-5} more assets", ) print( colored( - '\nUse --overwrite to replace existing files.', 'yellow' - ) + '\nUse --overwrite to replace existing files.', + 'yellow', + ), ) return False if dry_run: print( colored( - '\n✓ Dry run complete. No files were modified.', 'blue' - ) + '\n✓ Dry run complete. No files were modified.', + 'blue', + ), ) return True @@ -1266,7 +1303,8 @@ def install( src_bddl = os.path.join(pkg_root, 'bddl_files') if os.path.exists(src_bddl): dest_bddl = os.path.join( - self.bddl_root, manifest.package_name + self.bddl_root, + manifest.package_name, ) if os.path.exists(dest_bddl) and overwrite: shutil.rmtree(dest_bddl) @@ -1278,7 +1316,8 @@ def install( src_init = os.path.join(pkg_root, 'init_files') if os.path.exists(src_init): dest_init = os.path.join( - self.init_root, manifest.package_name + self.init_root, + manifest.package_name, ) if os.path.exists(dest_init) and overwrite: shutil.rmtree(dest_init) @@ -1301,18 +1340,19 @@ def install( if problem_file.endswith('.py'): src_file = os.path.join(src_problems, problem_file) dest_file = os.path.join( - dest_problems, problem_file + dest_problems, + problem_file, ) if os.path.exists(dest_file) and not overwrite: print( - f' ⚠ Problem file exists (skipped): {problem_file}' + f' ⚠ Problem file exists (skipped): {problem_file}', ) continue shutil.copy2(src_file, dest_file) print( - f' ✓ Problem class installed: {problem_file}' + f' ✓ Problem class installed: {problem_file}', ) # Install assets (including scene files) @@ -1320,13 +1360,15 @@ def install( src_assets = os.path.join(pkg_root, 'assets') if os.path.exists(src_assets): installed_count = 0 + skipped_count = 0 # Recursively copy all assets for root, dirs, files in os.walk(src_assets): # Calculate relative path from src_assets rel_root = os.path.relpath(root, src_assets) dest_root = os.path.join( - self.assets_root, rel_root + self.assets_root, + rel_root, ) os.makedirs(dest_root, exist_ok=True) @@ -1338,23 +1380,36 @@ def install( if os.path.exists(dest_file): if overwrite: os.remove(dest_file) + elif skip_existing_assets: + skipped_count += 1 + continue else: continue shutil.copy2(src_file, dest_file) installed_count += 1 - print(f' ✓ Assets installed: {installed_count} files') + if skip_existing_assets and skipped_count > 0: + print( + f' ✓ Assets: {installed_count} installed, {skipped_count} skipped (already exist)', + ) + else: + print( + f' ✓ Assets installed: {installed_count} files', + ) print( colored( - f'\n✓ Installation complete: {manifest.package_name}', 'green' - ) + f'\n✓ Installation complete: {manifest.package_name}', + 'green', + ), ) return True def uninstall( - self, package_name: str, remove_assets: bool = False + self, + package_name: str, + remove_assets: bool = False, ) -> bool: """ Uninstall a task package. @@ -1412,7 +1467,10 @@ def __init__(self, repo_id: str): self.repo_id = repo_id self.api = HfApi() self.cache_dir = os.path.join( - os.path.expanduser('~'), '.cache', 'vla_arena', 'packages' + os.path.expanduser('~'), + '.cache', + 'vla_arena', + 'packages', ) os.makedirs(self.cache_dir, exist_ok=True) @@ -1490,7 +1548,8 @@ def upload_with_git( # Copy package file print(f' Copying {package_name}.vlap...') dest_path = os.path.join( - packages_dir, f'{package_name}{PACKAGE_EXTENSION}' + packages_dir, + f'{package_name}{PACKAGE_EXTENSION}', ) shutil.copy2(package_path, dest_path) @@ -1655,7 +1714,8 @@ def download( # Download from HuggingFace local_path = os.path.join( - output_dir, f'{package_name}{PACKAGE_EXTENSION}' + output_dir, + f'{package_name}{PACKAGE_EXTENSION}', ) self.api.hf_hub_download( @@ -1668,7 +1728,9 @@ def download( # Move to expected location downloaded_path = os.path.join( - output_dir, 'packages', f'{package_name}{PACKAGE_EXTENSION}' + output_dir, + 'packages', + f'{package_name}{PACKAGE_EXTENSION}', ) if os.path.exists(downloaded_path) and downloaded_path != local_path: shutil.move(downloaded_path, local_path) @@ -1680,7 +1742,8 @@ def download_and_install( self, package_name: str, overwrite: bool = False, - token: str | None = None, + skip_existing_assets: bool = False, + token: str = None, ) -> bool: """ Download and install a package in one step. @@ -1688,6 +1751,7 @@ def download_and_install( Args: package_name: Name of the package overwrite: Whether to overwrite existing files + skip_existing_assets: Skip existing assets but install new ones token: HuggingFace API token Returns: @@ -1695,7 +1759,11 @@ def download_and_install( """ package_path = self.download(package_name, token=token) installer = TaskInstaller() - return installer.install(package_path, overwrite=overwrite) + return installer.install( + package_path, + overwrite=overwrite, + skip_existing_assets=skip_existing_assets, + ) # ============================================================================= @@ -1735,41 +1803,58 @@ def main(): pack_parser = subparsers.add_parser('pack', help='Pack a single task') pack_parser.add_argument('bddl_path', help='Path to BDDL file') pack_parser.add_argument( - '-o', '--output', default='.', help='Output directory' + '-o', + '--output', + default='.', + help='Output directory', ) pack_parser.add_argument('--init', help='Path to init file') pack_parser.add_argument('--name', help='Package name') pack_parser.add_argument('--author', default='', help='Author name') pack_parser.add_argument('--email', default='', help='Author email') pack_parser.add_argument( - '--description', default='', help='Task description' + '--description', + default='', + help='Task description', ) pack_parser.add_argument( - '--no-assets', action='store_true', help='Skip including assets' + '--no-assets', + action='store_true', + help='Skip including assets', ) # Pack suite command suite_parser = subparsers.add_parser( - 'pack-suite', help='Pack a task suite' + 'pack-suite', + help='Pack a task suite', ) suite_parser.add_argument('suite_name', help='Name of the task suite') suite_parser.add_argument( - '-o', '--output', default='.', help='Output directory' + '-o', + '--output', + default='.', + help='Output directory', ) suite_parser.add_argument('--author', default='', help='Author name') suite_parser.add_argument('--email', default='', help='Author email') suite_parser.add_argument( - '--description', default='', help='Suite description' + '--description', + default='', + help='Suite description', ) # Install command install_parser = subparsers.add_parser('install', help='Install a package') install_parser.add_argument('package_path', help='Path to .vlap package') install_parser.add_argument( - '--overwrite', action='store_true', help='Overwrite existing files' + '--overwrite', + action='store_true', + help='Overwrite existing files', ) install_parser.add_argument( - '--skip-assets', action='store_true', help='Skip installing assets' + '--skip-assets', + action='store_true', + help='Skip installing assets', ) install_parser.add_argument( '--dry-run', @@ -1783,7 +1868,8 @@ def main(): # Upload command upload_parser = subparsers.add_parser( - 'upload', help='Upload to HuggingFace Hub' + 'upload', + help='Upload to HuggingFace Hub', ) upload_parser.add_argument('package_path', help='Path to .vlap package') upload_parser.add_argument( @@ -1792,7 +1878,9 @@ def main(): help='HuggingFace repo ID (e.g., username/task-assets)', ) upload_parser.add_argument( - '--private', action='store_true', help='Make repo private' + '--private', + action='store_true', + help='Make repo private', ) upload_parser.add_argument('--token', help='HuggingFace API token') upload_parser.add_argument( @@ -1803,7 +1891,8 @@ def main(): # Download command download_parser = subparsers.add_parser( - 'download', help='Download from HuggingFace Hub' + 'download', + help='Download from HuggingFace Hub', ) download_parser.add_argument('package_name', help='Name of the package') download_parser.add_argument( @@ -1813,7 +1902,9 @@ def main(): ) download_parser.add_argument('-o', '--output', help='Output directory') download_parser.add_argument( - '--install', action='store_true', help='Install after download' + '--install', + action='store_true', + help='Install after download', ) download_parser.add_argument( '--overwrite', @@ -1832,11 +1923,14 @@ def main(): # Uninstall command uninstall_parser = subparsers.add_parser( - 'uninstall', help='Uninstall a package' + 'uninstall', + help='Uninstall a package', ) uninstall_parser.add_argument('package_name', help='Name of the package') uninstall_parser.add_argument( - '--remove-assets', action='store_true', help='Also remove assets' + '--remove-assets', + action='store_true', + help='Also remove assets', ) args = parser.parse_args() @@ -1887,7 +1981,7 @@ def main(): print(f' Init files: {len(manifest.init_files)}') print(f' Assets: {len(manifest.assets)}') print( - f' Total size: {manifest.total_size_bytes / 1024 / 1024:.2f} MB' + f' Total size: {manifest.total_size_bytes / 1024 / 1024:.2f} MB', ) print( f"\nObjects: {', '.join(manifest.objects[:10])}"